Compare commits

..

605 Commits

Author SHA1 Message Date
Matthias
ab3e3797a5 Merge pull request #2205 from freqtrade/master_add_2199
Release 2019.8-1 - hotfix data-dir not including exchange
2019-08-31 19:13:05 +02:00
Matthias
7c36e571d2 version bump to 2019.8-1 2019-08-31 15:38:38 +02:00
hroff-1902
040ba5662c Merge pull request #2199 from freqtrade/fix_datadir_init
Fix datadir init to always include exchange
2019-08-31 15:37:29 +02:00
Matthias
9634e516a9 Merge pull request #2196 from freqtrade/new_release
New release 2018-8
2019-08-28 19:26:35 +02:00
Matthias
44780837f1 Version bump to 2019-8 2019-08-28 06:33:10 +02:00
Matthias
95920f3b6b Merge pull request #2177 from freqtrade/fix/stoplosshandling
[minor]improvements to stoploss-on-exchange handling
2019-08-25 09:33:39 +02:00
Matthias
365b9c3e9c Add test to correctly handle unsuccessfull ordercreation 2019-08-24 18:06:33 +02:00
Matthias
3f6eeda3f0 Reset stoploss_order_id when recreating fails 2019-08-24 18:06:14 +02:00
Matthias
3820a38e79 Merge pull request #2175 from hroff-1902/hyperopt-split-backtesting
Hyperopt redesign
2019-08-24 14:39:46 +02:00
Matthias
60bc9f4f5e Merge pull request #2173 from freqtrade/improve/trailing_validation
improve stoploss validation
2019-08-24 09:15:43 +02:00
Matthias
a8842f38ca Fix wrong exception message 2019-08-24 09:08:08 +02:00
hroff-1902
667a623310 adjust tests 2019-08-24 00:10:55 +03:00
hroff-1902
067208bc9d make backtesting an attribute of Hyperopt 2019-08-24 00:10:35 +03:00
Matthias
70ebd09de4 Add checks verifying that stoploss is not 0 (and positive-stoploss is
also not 0).
2019-08-22 20:04:44 +02:00
Matthias
782f4112cd Add test checking stoploss == 0 values 2019-08-22 19:49:30 +02:00
Matthias
447bcf98e1 Merge pull request #2172 from hroff-1902/exchange-cosmetics
exchange cosmetics
2019-08-22 19:18:22 +02:00
hroff-1902
d19b11a00f exchange cosmetics 2019-08-22 20:01:41 +03:00
Matthias
ad6de07d2b Merge pull request #2155 from jraviotta/analysis
split example notebooks
2019-08-22 15:54:08 +02:00
Matthias
0e81d7204c Clense jupyter notebook 2019-08-22 15:43:39 +02:00
Matthias
91b0394433 Merge pull request #2156 from freqtrade/remove_live
Remove deprecated option live  - deprecate -r
2019-08-22 15:33:39 +02:00
Matthias
b2ef8f4e14 Add additional header 2019-08-22 15:26:18 +02:00
Matthias
81925dfadf Fix some doc inconsistencies 2019-08-22 13:01:10 +02:00
Matthias
098159ad41 Merge pull request #2170 from freqtrade/fix/docboxes
Fix documentation boxes
2019-08-22 12:44:35 +02:00
Matthias
fe12d2e3b7 Fix documentation syntax 2019-08-22 06:57:32 +02:00
Matthias
df1f57392c use seperate job for doc test 2019-08-22 06:56:41 +02:00
Matthias
949ca1abf8 Fail travis if doc-test fails 2019-08-22 06:53:51 +02:00
Matthias
e52d5e32aa Merge pull request #2067 from freqtrade/align_userdata
Align userdata usage
2019-08-21 19:55:42 +02:00
Matthias
aaeeb9c0c6 Merge branch 'develop' into align_userdata 2019-08-21 19:41:10 +02:00
Matthias
d2958fc0f5 Merge pull request #2168 from freqtrade/fix/downloadscript_pairs
Fix downloadscript pair handling
2019-08-21 09:09:03 +02:00
Matthias
f8235aec74 Merge pull request #2167 from hroff-1902/fix-download-script
minor: fix download replacement script
2019-08-21 07:03:13 +02:00
Matthias
13ffb39245 Adjust tests to fixed loading method 2019-08-21 06:59:07 +02:00
Matthias
75b2db4424 FIx loading pairs-list 2019-08-21 06:58:56 +02:00
hroff-1902
14aaf8976f fix download replacement script 2019-08-21 02:26:58 +03:00
Matthias
eebf39a1df Merge pull request #2165 from freqtrade/xmatthias-patch-1
Fix grammar error in documentation
2019-08-20 19:40:07 +02:00
Matthias
210f66e48b Improve wording 2019-08-20 19:34:18 +02:00
Matthias
91e72ba081 small formatting issue 2019-08-20 19:32:26 +02:00
Matthias
be308ff914 Fix grammar error in documentation 2019-08-20 09:45:28 +02:00
Matthias
4ee35438a7 Improve deprecated docs 2019-08-20 07:07:05 +02:00
Matthias
11dab2b9ca Deprecate documentation for --refresh-pairs-cached 2019-08-20 07:02:30 +02:00
Matthias
f02adf2a45 Deprecate --refresh-pairs-cached 2019-08-20 07:00:43 +02:00
Matthias
9e24992835 Remove calls to load_data using live= 2019-08-20 07:00:43 +02:00
Matthias
e9e2a83436 remove --live references 2019-08-20 07:00:43 +02:00
Matthias
af51ff4162 Merge pull request #2146 from freqtrade/download_module
Download module
2019-08-20 06:59:30 +02:00
Matthias
e8ee087e9d Merge branch 'develop' into download_module 2019-08-20 06:49:18 +02:00
Jonathan Raviotta
8cc477f353 edits 2019-08-20 00:47:10 -04:00
Matthias
c63856dac4 Merge pull request #2158 from freqtrade/config_consistency
Config consistency checking improvements
2019-08-20 06:44:41 +02:00
Matthias
8d1a575a9b Reword documentation 2019-08-20 06:39:28 +02:00
Matthias
9e8ca8d4bf Merge pull request #2138 from freqtrade/history_docstrings
Refactorings to history
2019-08-20 06:35:54 +02:00
Matthias
491d742bf9 Merge pull request #2163 from hroff-1902/dataprovider-get-pair-dataframe
get_pair_dataframe(): example in the docs changed
2019-08-20 06:33:59 +02:00
Matthias
dc35a8022b Merge pull request #2157 from freqtrade/fix/create_order_crash
create market order crash if exchange raises an exception
2019-08-20 06:22:43 +02:00
hroff-1902
70b1a05d97 example in the docs changed 2019-08-20 01:32:02 +03:00
Matthias
785c3e9e61 Merge pull request #2161 from freqtrade/dependabot/pip/develop/ccxt-1.18.1068
Bump ccxt from 1.18.1063 to 1.18.1068
2019-08-19 16:41:07 +02:00
dependabot-preview[bot]
9ad9ce0da1 Bump ccxt from 1.18.1063 to 1.18.1068
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1063 to 1.18.1068.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1063...1.18.1068)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-19 10:52:53 +00:00
Matthias
042e47543c Merge pull request #2159 from freqtrade/fix/pairlist_logging
Fix pairlist logging
2019-08-19 09:48:42 +02:00
Matthias
71d612f6e4 Merge pull request #2160 from freqtrade/fix/dryrun_crashes
Gracefully handle problems with dry-run orders
2019-08-19 09:06:44 +02:00
Matthias
a4ede02ced Gracefully handle problems with dry-run orders 2019-08-18 19:38:23 +02:00
Matthias
ea4db0ffb6 Pass object-name to loader to fix logging 2019-08-18 18:11:34 +02:00
Matthias
d785d76370 make VolumePairlist less verbose
no need to print the full whitelist on every iteration
2019-08-18 18:11:24 +02:00
Matthias
b6462cd51f Add explaining comment 2019-08-18 16:22:18 +02:00
Matthias
611850bf91 Add edge/dynamic_whitelist validation 2019-08-18 16:19:24 +02:00
Matthias
ddfadbb69e Validate configuration consistency after loading strategy 2019-08-18 16:10:10 +02:00
Matthias
045ac1019e Split test for buy-orders too 2019-08-18 15:58:53 +02:00
Matthias
ee7ba96e85 Don't do calculations in exception handlers when one element can be None
fixes #2011
2019-08-18 15:46:38 +02:00
Matthias
8e96ac8765 Split exception tests for create_order 2019-08-18 15:45:30 +02:00
Matthias
acf1e734ec Adapt lg_has calls to new standard 2019-08-18 15:09:44 +02:00
Matthias
0a478bc0dc Merge branch 'develop' into align_userdata 2019-08-18 15:00:12 +02:00
Matthias
9005447590 Merge pull request #2149 from hroff-1902/dataprovider-get-pair-dataframe
Dataprovider: get_pair_dataframe() helper method, cleanup
2019-08-18 13:57:49 +02:00
hroff-1902
d300964691 code formatting in test_dataprovider.py 2019-08-18 13:06:21 +03:00
hroff-1902
407a3bca62 implementation of ohlcv optimized 2019-08-18 13:00:37 +03:00
hroff-1902
310e438706 logging message improved 2019-08-18 12:55:31 +03:00
hroff-1902
8a2a8ab8b5 docstring for ohlcv improved 2019-08-18 12:47:19 +03:00
Matthias
5e440a4cdc Improve docs to point to freqtrade download-data 2019-08-18 06:55:19 +02:00
Matthias
3a1b641db1 Merge pull request #2154 from freqtrade/doc/docker_updatefreq
[minor] Explain docker image rebuilding
2019-08-18 06:40:30 +02:00
Jonathan Raviotta
2cffc3228a split example notebooks 2019-08-17 19:37:34 -04:00
Matthias
7fa6d804ce Add note explaining how / when docker images are rebuild 2019-08-17 19:48:55 +02:00
Matthias
a398eea244 Merge pull request #2153 from freqtrade/enable/dependabot
Enable/dependabot
2019-08-17 19:40:36 +02:00
Matthias
0e87cc8c84 Remove pyup.yml 2019-08-17 19:30:03 +02:00
Matthias
764bab8eb9 Merge pull request #2152 from freqtrade/dependabot/pip/ccxt-1.18.1063
Bump ccxt from 1.18.1043 to 1.18.1063
2019-08-17 19:29:24 +02:00
Matthias
351740fc80 Change pyup to every month (should ideally not find anything ...) 2019-08-17 17:27:14 +02:00
dependabot-preview[bot]
9143ea13ad Bump ccxt from 1.18.1043 to 1.18.1063
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1043 to 1.18.1063.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1043...1.18.1063)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-17 15:26:07 +00:00
Matthias
4711d66cab Merge pull request #2150 from freqtrade/dependabot/pip/pytest-5.1.0
Bump pytest from 5.0.1 to 5.1.0
2019-08-17 17:25:12 +02:00
Matthias
09967d4ff8 Merge pull request #2151 from freqtrade/dependabot/pip/sqlalchemy-1.3.7
Bump sqlalchemy from 1.3.6 to 1.3.7
2019-08-17 17:24:54 +02:00
Matthias
e0335705b2 Add dependabot config yaml 2019-08-17 17:19:02 +02:00
dependabot-preview[bot]
4ce3cc66d5 Bump sqlalchemy from 1.3.6 to 1.3.7
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.6 to 1.3.7.
- [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-preview[bot] <support@dependabot.com>
2019-08-17 15:14:01 +00:00
dependabot-preview[bot]
fce3d7586f Bump pytest from 5.0.1 to 5.1.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.0.1 to 5.1.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.0.1...5.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-17 15:13:39 +00:00
hroff-1902
cda912bd8c test added 2019-08-17 13:05:13 +03:00
hroff-1902
84a0f9ea42 get_pair_dataframe helper method added 2019-08-17 12:57:44 +03:00
Matthias
08fa5136e1 use copy of minimal_config ... 2019-08-17 07:19:46 +02:00
Matthias
7a79b292e4 Fix bug in pairs fallback resolving 2019-08-17 07:05:42 +02:00
Matthias
a53e9e3a98 improve tests for download_module 2019-08-17 07:01:20 +02:00
Matthias
f7d5280f47 Replace ARGS_DOWNLOADER with ARGS_DOWNLOAD_DATA 2019-08-17 06:48:34 +02:00
Matthias
29c56f4447 Replace download_backtest_data script with warning message 2019-08-17 06:48:31 +02:00
Matthias
c9207bcc00 Remove blank line at end 2019-08-16 16:01:30 +02:00
Matthias
132f28ad44 Add tests to correctly load / override pair-lists 2019-08-16 15:52:59 +02:00
Matthias
b2c215029d Add tests for download_data entrypoint 2019-08-16 15:28:11 +02:00
Matthias
89257832d7 Don't use internal _API methods 2019-08-16 15:27:59 +02:00
Matthias
219d0b7fb0 Adjust documentation to removed download-script 2019-08-16 15:27:48 +02:00
Matthias
4e308a1a3e Resolve pairlist in configuration 2019-08-16 14:56:57 +02:00
Matthias
3c15e3ebdd Default load minimal config 2019-08-16 14:56:38 +02:00
Matthias
8655e521d7 Adapt some tests 2019-08-16 14:53:46 +02:00
Matthias
05deb9e09b Migrate download-script logic to utils.py 2019-08-16 14:42:44 +02:00
Matthias
91886120a7 use nargs for --pairs argument 2019-08-16 14:39:29 +02:00
Matthias
09286d4918 file_dump_json accepts Path - so we should feed it that 2019-08-16 13:04:48 +02:00
Matthias
161db08745 Merge pull request #2142 from hroff-1902/hyperopt-print-json
Hyperopt: --print-json option
2019-08-16 11:08:54 +02:00
Matthias
8aaaab4163 Merge pull request #2145 from freqtrade/update_docker_image
Update dockerfile python version
2019-08-16 10:24:55 +02:00
Matthias
53db382695 Update dockerfile python version 2019-08-16 10:19:06 +02:00
Matthias
1b6051e4df Merge pull request #2144 from freqtrade/strategy_doc
Fix wrong warning box
2019-08-16 09:40:42 +02:00
Matthias
8d206f8308 Fix wrong warning box 2019-08-16 06:57:46 +02:00
hroff-1902
b94f3e80c4 tests fixed 2019-08-16 04:20:12 +03:00
hroff-1902
2a842778e3 tests added 2019-08-16 01:05:34 +03:00
hroff-1902
e525275d10 make flake and mypy happy 2019-08-15 23:13:46 +03:00
hroff-1902
4fa92ec0fa hyperopt: --print-json option added 2019-08-15 21:39:04 +03:00
Matthias
69eff89049 Improve comment in test_history to explain what is tested 2019-08-15 20:28:32 +02:00
Matthias
12677f2d42 Adjust docstring to match functioning of load_cached_data 2019-08-15 20:13:19 +02:00
Matthias
a94a89086f Don't forward timerange to load_ticker_file
when loading cached data for updating.
We always want to get all data, not just a fraction (we would end up
overwriting the non-loaded part of the data).
2019-08-15 20:09:00 +02:00
Matthias
80a71323cc Merge pull request #2141 from ahonnecke/fstring-runtime
f the string
2019-08-15 19:33:57 +02:00
Ashton Honnecke
fd77f699df f the string 2019-08-15 10:41:02 -06:00
Matthias
93cf2cd19b Merge pull request #2135 from freqtrade/ohlcv_docstring
[minor] Improve docstring for some downloading methods
2019-08-15 16:23:42 +02:00
Matthias
585536835a Merge pull request #2131 from freqtrade/lock_pairs
Lock pairs
2019-08-15 07:21:00 +02:00
Matthias
f5e437d8c7 Change create_trade to create_trades for new test 2019-08-15 06:59:45 +02:00
Matthias
14c4854987 Merge branch 'develop' into lock_pairs 2019-08-15 06:56:39 +02:00
Matthias
3af5691b91 Merge pull request #2124 from freqtrade/fix/sell_order_hanging
Fix/sell order hanging
2019-08-15 06:52:37 +02:00
Matthias
9f26c4ebdc Merge branch 'develop' into fix/sell_order_hanging 2019-08-15 06:46:12 +02:00
Matthias
11790fbf01 Fix typos in docstrings 2019-08-15 06:37:26 +02:00
Matthias
f3e6bcb20c Avoid using negative indexes 2019-08-15 06:35:50 +02:00
Matthias
e0e50115d2 Merge pull request #2136 from freqtrade/timerange_fix
[refactor] Move Timerange parsing to it's own class
2019-08-15 06:35:37 +02:00
Matthias
b2a22f1afb Fix samll errors 2019-08-14 21:39:53 +02:00
Matthias
9d3322df8c Adapt history-tests to new load_cached_data header 2019-08-14 20:49:13 +02:00
Matthias
91d1061c73 Abstract tickerdata storing 2019-08-14 20:49:06 +02:00
Matthias
0ffb184eba Change some docstrings and formatting from history 2019-08-14 20:45:24 +02:00
Matthias
096a6426db Override equality operator 2019-08-14 10:22:54 +02:00
Matthias
84baef922c Rename get_history to get_historic_ohlcv 2019-08-14 10:14:54 +02:00
Matthias
51c3a31bb5 Correct imports and calls to parse_timerange 2019-08-14 10:07:32 +02:00
Matthias
06fa07e73e Move parse_timerange to TimeRange class 2019-08-14 10:07:14 +02:00
Matthias
4da2bfefb7 Improve docstring for some downloading methods 2019-08-14 09:37:17 +02:00
Matthias
3b30aab8a7 Merge pull request #2132 from freqtrade/process_return_value
allow create_trade() to create multiple trades per iteration
2019-08-14 07:23:05 +02:00
Matthias
c2e9685e04 Merge pull request #2121 from hroff-1902/config-allow-comments
Allow comments in config files
2019-08-14 06:37:33 +02:00
Matthias
d6f5f6b7ba Add test with preexisting trades 2019-08-14 06:21:15 +02:00
Matthias
a4ab42560f improve docstring for create_trades 2019-08-14 06:16:59 +02:00
Matthias
a76136c010 Rename create_trade to create_trades 2019-08-14 06:16:43 +02:00
Matthias
e35a349229 Fix spelling of interface.py docstring 2019-08-14 06:07:03 +02:00
hroff-1902
3d36747b92 preface in configuration.md reworked 2019-08-13 21:52:50 +03:00
Matthias
c0784b7c33 Merge pull request #2089 from hroff-1902/hyperopt-print-colorized
Hyperopt print colorized results
2019-08-13 19:36:06 +02:00
Matthias
828315f675 Merge pull request #2130 from freqtrade/bad_exchanges
fail for bad exchanges
2019-08-13 19:34:35 +02:00
hroff-1902
4c4ba08e85 colorama added to install_requires 2019-08-13 19:47:38 +03:00
hroff-1902
94196c84e9 docs: explanation for --no-color and colorization schema for results 2019-08-13 14:25:56 +03:00
Matthias
9d476b5ab2 Also check 0 open trades 2019-08-13 10:34:27 +02:00
Matthias
0a07dfc5cf Add test verifying that multiple trades are opened in one iteration 2019-08-13 10:20:32 +02:00
Matthias
d69f7ae471 Adapt final tests to support multi-trade creation 2019-08-13 10:15:31 +02:00
Matthias
974d899b33 Adapt some more tests 2019-08-13 10:12:12 +02:00
Matthias
6948e0ba84 Handle orderbook_depth check correctly 2019-08-13 10:12:02 +02:00
Matthias
a325f1ce2b adapt some tests
since create_trade() can now buy multiple times, we need to use
execute_buy() to create a single trade
2019-08-13 10:01:43 +02:00
Matthias
997eb7574a Support creating multiple trades in one iteration 2019-08-13 10:01:29 +02:00
Matthias
8873e0072c process_maybe_execute_buy does not need to return bool 2019-08-13 09:42:22 +02:00
Matthias
c29389f5f3 Remove process() checks from tests 2019-08-13 09:38:21 +02:00
Matthias
4b8eaaf7aa freqtradebot.process() does not need to return anything 2019-08-13 09:37:56 +02:00
Matthias
8d813fa728 Remove return-value for _process 2019-08-13 09:36:52 +02:00
Matthias
28e318b646 Lock pairs for stoploss_on_exchange fills too 2019-08-13 08:47:11 +02:00
Matthias
2961efdc18 Initial test for locked pair 2019-08-13 08:38:19 +02:00
Matthias
3c589bb877 fail if known bad exchanges are detcted 2019-08-13 08:27:46 +02:00
Matthias
d8dbea9d5b Add exchange_reasons to bad exchanges 2019-08-13 08:20:35 +02:00
Matthias
f960ea039e Remove duplicate test 2019-08-13 08:05:51 +02:00
hroff-1902
de80234165 hyperopt options updated in bot-usage.md 2019-08-13 00:23:41 +03:00
hroff-1902
906be7be7c Merge branch 'develop' into config-allow-comments 2019-08-13 00:14:19 +03:00
hroff-1902
482847a994 docs adjusted; various fixes to bot-usage.md and configuration.md 2019-08-13 00:10:33 +03:00
hroff-1902
58d308fd05 fix handling --no-color for edge and backtesting 2019-08-12 23:13:04 +03:00
Matthias
59acd5ec7c Lock pair for the rest of the candle in case of sells 2019-08-12 20:39:34 +02:00
Matthias
ca739f71fb Fix default argument handling for timeframe_to_nextdate 2019-08-12 20:39:24 +02:00
Matthias
23a70932d2 Remove pointless tests (without config?? really?) 2019-08-12 20:36:45 +02:00
hroff-1902
1a34b9b61c --no-color option introduced 2019-08-12 21:08:34 +03:00
hroff-1902
8f92912852 final colorization schema
colorization schema-2: red, green, bright/dim

colorization schema-3: red, green, bright only green bests

colorization schema-4: no red, green for profit, bright for bests
2019-08-12 21:08:52 +03:00
Matthias
2600cb7b64 simplify timeframe_next_date calculation 2019-08-12 20:04:19 +02:00
Matthias
200b6ea10f Add is_pair_locked 2019-08-12 19:50:38 +02:00
Matthias
8c1efec43a Merge pull request #2125 from freqtrade/pyup/scheduled-update-2019-08-12
Scheduled weekly dependency update for week 32
2019-08-12 17:41:25 +02:00
pyup-bot
dd30d74688 Update python-rapidjson from 0.7.2 to 0.8.0 2019-08-12 15:25:09 +00:00
pyup-bot
6f42d6658f Update arrow from 0.14.4 to 0.14.5 2019-08-12 15:25:08 +00:00
pyup-bot
c4cdd85e80 Update ccxt from 1.18.1021 to 1.18.1043 2019-08-12 15:25:06 +00:00
pyup-bot
0bd71db5df Update scipy from 1.3.0 to 1.3.1 2019-08-12 15:25:05 +00:00
Matthias
feced71a6d Test closing sell-orders immediately 2019-08-12 16:47:00 +02:00
Matthias
444ee274d7 close dry-run orders in case of market orders 2019-08-12 16:46:45 +02:00
Matthias
bb0b160001 Remove duplicate test 2019-08-12 16:39:21 +02:00
Matthias
241d510096 Handle and update sell-orders immediately if they are closed 2019-08-12 16:34:55 +02:00
Matthias
c042d08bb7 Add lock_pairs to interface 2019-08-12 16:29:09 +02:00
Matthias
1ce63b5b42 Reformat tests to be easier readable 2019-08-12 16:25:01 +02:00
Matthias
dd0ba183f8 Add timeframe_to_prev_candle 2019-08-12 16:11:43 +02:00
Matthias
933a553dd4 Convert timeframe to next date 2019-08-12 16:08:23 +02:00
Matthias
af67bbde31 Test timeframe_to_x 2019-08-12 15:43:10 +02:00
Matthias
6310b40fc6 Merge pull request #2123 from freqtrade/hyperoptloss_help
[minor] Improve hyperopt-loss docs
2019-08-12 14:08:32 +02:00
Matthias
2463a4af2a Merge pull request #2120 from freqtrade/log_has_ref
[minor, tests] - use caplog instead of caplog.record_tuples
2019-08-12 07:02:10 +02:00
Matthias
51ad8f5ab4 Merge branch 'develop' into log_has_ref 2019-08-12 06:49:41 +02:00
Matthias
615ce6aa69 Merge pull request #2118 from freqtrade/config_standalone
Config standalone loading
2019-08-12 06:47:52 +02:00
Matthias
43b41324e2 Improve hyperopt-loss docs 2019-08-12 06:45:27 +02:00
Matthias
91b0db138a Merge pull request #2122 from hroff-1902/hyperopt-cleanup3
Minor: cosmetics in sample_hyperopt and default_hyperopt
2019-08-12 06:41:00 +02:00
Matthias
197ce0b670 Improve documentation wording for multiconfig files 2019-08-12 06:35:47 +02:00
Matthias
002003292e Merge branch 'develop' into log_has_ref 2019-08-12 06:34:49 +02:00
Matthias
0b367a14f1 Merge pull request #2119 from freqtrade/disable_sloE_dry
Disable stoploss on exchange during dry-runs
2019-08-12 06:12:22 +02:00
hroff-1902
e5dcd520ba cosmetics in sample_hyperopt and default_hyperopt 2019-08-12 02:19:50 +03:00
hroff-1902
90b75afdb1 test added to load config with comments and trailing commas 2019-08-12 00:33:34 +03:00
hroff-1902
2d60e4b18b allow comments and trailing commas in config files 2019-08-12 00:32:03 +03:00
Matthias
c5d8499ad2 Improve documentation regarding tests 2019-08-11 20:30:15 +02:00
Matthias
b77c0d2813 Replace all "logentry" in caplog_record_tuples
use log_has to have checking log-entries standardized.
2019-08-11 20:22:50 +02:00
Matthias
a636dda07d Fix remaining tests using log_has 2019-08-11 20:17:39 +02:00
Matthias
dc5719e1f4 Adapt rpc to new log_has method 2019-08-11 20:17:22 +02:00
Matthias
d53f63023a Change log_has to get caplog instead of caplog.record_tuples in more
tests
2019-08-11 20:16:52 +02:00
Matthias
0221607318 Change log_has for some tests 2019-08-11 20:16:34 +02:00
Matthias
a1b5c7242e Change log-has to use record_tuples itself 2019-08-11 20:14:58 +02:00
Matthias
a225672c87 Add tests for dry-run stoposs_on_exchange 2019-08-11 19:45:31 +02:00
Matthias
4b4fcc7034 Change stoploss_on_exchange in freqtradebot 2019-08-11 19:43:57 +02:00
Matthias
85094a59e6 Merge pull request #2063 from hroff-1902/remove-pytest-warning2
tests: don't mask numpy errors as warnings in tests
2019-08-11 19:29:27 +02:00
Matthias
e02e64fc07 Add test to make sure dry-run disables stoploss on exchange 2019-08-11 14:15:04 +02:00
Matthias
176beefa88 Disable stoploss on exchange for dry-runs 2019-08-11 14:14:51 +02:00
Matthias
1a85e3b4cd Fix numpy warning 2019-08-11 13:48:41 +02:00
hroff-1902
5209ce5bfa tests: don't mask numpy errors as warnings in tests 2019-08-11 13:46:41 +02:00
Matthias
2c5a499a8b Merge branch 'develop' into align_userdata 2019-08-10 20:15:07 +02:00
Matthias
6d89da45b0 Add test for from_config 2019-08-10 20:02:11 +02:00
Matthias
eb328037b7 combine normalize method and config validation to in_files 2019-08-10 19:58:04 +02:00
Matthias
afba31c3f9 change method from _load_config_Files to from_files() 2019-08-10 19:57:49 +02:00
Matthias
c4cbe79b48 Adjust documentation 2019-08-10 19:55:33 +02:00
Matthias
8ba7657007 Merge pull request #2117 from hroff-1902/config-load-config
Minor configuration cleanup
2019-08-10 19:34:03 +02:00
hroff-1902
48d8376878 tests fixed 2019-08-10 18:47:58 +03:00
Matthias
74e583a612 Merge pull request #2094 from hroff-1902/hyperopt-roi-stoploss
Simplify custom hyperopts -- no need to copy ugly methods in every custom implementation
2019-08-10 15:49:52 +02:00
Matthias
29619ccf1c Merge pull request #2108 from jraviotta/nbdocs
Added jupyter notebook example and doc edits
2019-08-10 15:47:06 +02:00
Matthias
ab092fc77f Reinstate comment on backesting data 2019-08-10 15:45:41 +02:00
hroff-1902
28d8fc871a tests adjusted 2019-08-10 16:07:30 +03:00
hroff-1902
ad6a249832 download_backtest_data.py adjusted 2019-08-10 15:14:37 +03:00
hroff-1902
50c9679e23 move load_config_file() to separate module 2019-08-10 14:24:14 +03:00
Jonathan Raviotta
8eb39178ea code block instructions. removed extra packages 2019-08-09 17:24:17 -04:00
Jonathan Raviotta
dd35ba5e81 added imports to doc code blocks. 2019-08-09 17:06:19 -04:00
Jonathan Raviotta
3cc772c8e9 added reminders 2019-08-09 11:53:29 -04:00
Jonathan Raviotta
247d7475e1 fixes to example notebook. 2019-08-09 11:41:05 -04:00
Jonathan Raviotta
51d59e673b fixed another instance of Path in docs and nb 2019-08-09 11:36:53 -04:00
hroff-1902
ae39f6fba5 use of termcolor eliminated 2019-08-09 14:51:03 +03:00
hroff-1902
15cf5ac2d7 docs improved 2019-08-09 09:31:30 +03:00
Matthias
de99942499 Merge pull request #2114 from CedricSchmeits/negativeSharpeLoss
As -sharp_ratio is returned the value should be nagative.
2019-08-09 06:19:59 +02:00
Jonathan Raviotta
ccf3c69874 edits to clarify backtesting analysis 2019-08-08 22:09:15 -04:00
Cedric Schmeits
8ad5afd3a1 As -sharp_ratio is returned the value should be nagative.
This leads in a high positive result of the loss function, as it is a minimal optimizer
2019-08-08 22:10:51 +02:00
hroff-1902
0d4a2c6c3a advanced sample hyperopt added; changes to helpstrings 2019-08-08 22:51:37 +03:00
Matthias
02b2de5c73 Merge pull request #2113 from freqtrade/improve_setup.sh
Improve setup.sh
2019-08-08 11:14:51 +02:00
Jonathan Raviotta
2bc67b4a96 missed a call of os.path. removed it. 2019-08-07 20:47:37 -04:00
Jonathan Raviotta
9df1c23c71 changed Path, added jupyter 2019-08-07 19:48:55 -04:00
Matthias
7a47d81b7b Ensure git reset --hard is realy desired 2019-08-07 21:45:58 +02:00
Matthias
831e708897 Detect virtualenv and quit in that case 2019-08-07 21:45:45 +02:00
Matthias
757538f114 Run ldconfig to add /usr/local/lib to path 2019-08-07 21:35:52 +02:00
Matthias
cc4900f66c Doublecheck if virtualenv IS present 2019-08-07 21:19:16 +02:00
Matthias
7d02580a2b setup.sh script shall fail if venv initialization fails 2019-08-07 21:03:03 +02:00
Matthias
3d3b0938e5 Merge pull request #2101 from freqtrade/backtest_ticker_interval_unset
Backtest ticker interval unset
2019-08-07 14:20:36 +02:00
Matthias
9c5773ca0a Merge pull request #2111 from freqtrade/pyup-update-plotly-4.0.0-to-4.1.0
Update plotly to 4.1.0
2019-08-07 10:13:08 +02:00
Matthias
092776442b Merge pull request #2109 from freqtrade/pyup-update-mkdocs-material-3.1.0-to-4.4.0
Update mkdocs-material to 4.4.0
2019-08-07 09:57:33 +02:00
Matthias
0267976044 Merge pull request #2110 from freqtrade/pyup-update-ccxt-1.18.1008-to-1.18.1021
Update ccxt to 1.18.1021
2019-08-07 09:57:01 +02:00
pyup-bot
5864968ce9 Update plotly from 4.0.0 to 4.1.0 2019-08-07 07:02:25 +00:00
pyup-bot
33bc8a2404 Update ccxt from 1.18.1008 to 1.18.1021 2019-08-07 07:02:19 +00:00
pyup-bot
dfce202034 Update mkdocs-material from 3.1.0 to 4.4.0 2019-08-07 07:02:15 +00:00
Matthias
ea46bb3b84 Merge pull request #2103 from freqtrade/since_int
Since arguments are in milliseconds integer throughout ccxt.
2019-08-07 06:19:26 +02:00
Jonathan Raviotta
8418dfbaed edits for jupyter notebook example 2019-08-06 22:35:14 -04:00
Matthias
caf4580346 Use UTC Timezone for test 2019-08-06 20:23:32 +02:00
Matthias
a90ced1f38 Since arguments are in milliseconds integer throughout ccxt.
Explained here: https://github.com/ccxt/ccxt/issues/5636

fixes #2093
2019-08-06 20:09:09 +02:00
Matthias
6c0c77b3a1 Merge pull request #2096 from freqtrade/fix/cons_buys_1971
Evaluate current candle during backtesting
2019-08-06 13:46:16 +02:00
Matthias
16d4a4723f Merge pull request #2102 from freqtrade/optimize/travis
Update install-script to use parameter
2019-08-06 13:38:32 +02:00
Matthias
327e653fae Merge pull request #2100 from freqtrade/strategy_list_doc
Fix documentation for strategy-list
2019-08-06 13:31:11 +02:00
Matthias
81f773054d Add test to verify ticker_inteval is set 2019-08-06 06:56:08 +02:00
Matthias
7e91a0f4a8 Fail gracefully if ticker-interval is not set 2019-08-06 06:45:44 +02:00
Matthias
9d471f3c9a Fix documentation for strategy-list 2019-08-06 06:32:31 +02:00
Matthias
7e46a9833b Merge pull request #2097 from freqtrade/urllib3
Update urllib to latest version
2019-08-06 06:05:29 +02:00
Matthias
988a0245c2 Update install-script to use parameter
Use --prefix /usr/local for install-script too
2019-08-05 20:37:38 +02:00
Matthias
0376630f7a Update urllib to latest version 2019-08-05 20:25:20 +02:00
Matthias
c7d0329754 Clean up comments of detail-backtests 2019-08-05 20:19:19 +02:00
Matthias
bc2e920ae2 Adjust code to verify "current" candle for buy/sells 2019-08-05 20:07:29 +02:00
Matthias
3721610a63 Add new detailed trade-scenario tests
covers cases raised in #1971
2019-08-05 20:06:42 +02:00
Matthias
e060516cc7 Merge pull request #2049 from jraviotta/conda
Conda / makefile
2019-08-05 19:49:25 +02:00
Matthias
20abd4b833 Merge pull request #2095 from freqtrade/pyup/scheduled-update-2019-08-05
Scheduled weekly dependency update for week 31
2019-08-05 19:28:42 +02:00
Matthias
904381058c Add documentation for conda install 2019-08-05 19:25:43 +02:00
pyup-bot
5e64d629a3 Update coveralls from 1.8.1 to 1.8.2 2019-08-05 15:26:19 +00:00
pyup-bot
d71102c45a Update py_find_1st from 1.1.3 to 1.1.4 2019-08-05 15:26:17 +00:00
pyup-bot
403f7668d5 Update jsonschema from 3.0.1 to 3.0.2 2019-08-05 15:26:16 +00:00
pyup-bot
930c25f7f1 Update scikit-learn from 0.21.2 to 0.21.3 2019-08-05 15:26:11 +00:00
pyup-bot
187d029d20 Update arrow from 0.14.3 to 0.14.4 2019-08-05 15:26:10 +00:00
pyup-bot
9914198a6c Update ccxt from 1.18.992 to 1.18.1008 2019-08-05 15:26:09 +00:00
hroff-1902
c6444a10a8 move roi_space, stoploss_space, generate_roi_table to IHyperOpt 2019-08-05 18:07:25 +03:00
Matthias
383b24ab84 Merge branch 'develop' into align_userdata 2019-08-05 06:55:51 +02:00
hroff-1902
9cbab35de0 colorization by means of termcolor and colorama 2019-08-04 22:54:19 +03:00
Matthias
eeecdd4e5a Merge pull request #2092 from freqtrade/split_analyze_ticker
Split analyze_ticker
2019-08-04 19:37:52 +02:00
Matthias
2af663dccb rename _analyze_ticker_int to _analyze_ticker_internal 2019-08-04 12:55:03 +02:00
Matthias
0be7e2ef70 Merge pull request #2090 from freqtrade/fix/plotting_DB
load_trades_db should give as many columns as possible
2019-08-04 12:52:39 +02:00
Matthias
4d1ce8178c intend if to be clearer 2019-08-04 10:38:37 +02:00
Matthias
c5ccf44750 Remove generate_dataframe from plot_dataframe script 2019-08-04 10:26:04 +02:00
Matthias
e4380b533b Print plot filename so it can be easily opened 2019-08-04 10:25:46 +02:00
Matthias
62262d0bb5 improve docstring of _analyze_ticker_int 2019-08-04 10:21:22 +02:00
Matthias
52d92cba90 Split analyze_ticker and _analyze_ticker_int 2019-08-04 10:20:31 +02:00
Matthias
0df5932593 Merge pull request #2091 from freqtrade/adjust_issuetemplate
add Operating system to issue template
2019-08-04 09:32:56 +02:00
Matthias
d1838dceec Merge pull request #2086 from freqtrade/fix_restricted_markets
Restricted pairs warning
2019-08-04 09:25:59 +02:00
Matthias
c6bd143785 add Operating system to issue template 2019-08-03 20:04:49 +02:00
Matthias
d51fd1a5d0 fix typo 2019-08-03 19:56:41 +02:00
Matthias
c4e30862ee load_trades_db should give as many columns as possible 2019-08-03 19:55:54 +02:00
hroff-1902
3dd6fe2703 wording 2019-08-03 19:44:32 +03:00
hroff-1902
fe796c46c3 test adjusted 2019-08-03 19:13:18 +03:00
hroff-1902
f200f52a16 hyperopt print colorized results 2019-08-03 19:09:42 +03:00
Matthias
d59608f764 adjust some documentation wordings 2019-08-03 17:19:37 +02:00
Matthias
b3e6e710d8 Merge pull request #2084 from hroff-1902/hyperopt-print-params4
Improvements to hyperopt output
2019-08-03 13:24:47 +02:00
Matthias
8ab07e0451 Add FAQ section about restricted markets 2019-08-03 13:22:44 +02:00
Matthias
ad55faafa8 Fix odd test 2019-08-03 13:18:37 +02:00
Matthias
bbd58e772e Warn when using restricted pairs
As noted in https://github.com/ccxt/ccxt/issues/5624, there is currently
no way to detect if a user is impacted by this or not prior to creating
a order.
2019-08-03 13:14:36 +02:00
hroff-1902
e8b2ae0b85 tests adjusted 2019-08-03 11:34:09 +03:00
hroff-1902
13620df717 'with values:' line removed 2019-08-03 11:05:05 +03:00
Matthias
fb103dd162 Merge pull request #2085 from hroff-1902/remove-pytest-warning6
tests: hide deprecation warning due to use of --live
2019-08-03 09:35:22 +02:00
hroff-1902
3b65c986ee wordings fixed 2019-08-03 10:20:20 +03:00
hroff-1902
cad7d9135a tests: hide deprecation warning due to use of --live 2019-08-03 09:24:27 +03:00
hroff-1902
b152d1a7ab docs agjusted, plus minor fixes 2019-08-02 22:23:48 +03:00
hroff-1902
aa8f44f68c improvements to hyperopt output 2019-08-02 22:22:58 +03:00
Matthias
1810d86555 Merge pull request #2080 from freqtrade/add_strategy_docs
docs: Create detailed section about strategy problem analysis
2019-08-02 20:29:09 +02:00
Matthias
39e8e507d9 Merge branch 'develop' into align_userdata 2019-08-02 20:08:26 +02:00
Matthias
3eb571f34c recommended ... 2019-08-02 20:04:18 +02:00
Matthias
e8be357624 Merge pull request #2079 from hroff-1902/hyperopt-print-params3
minor: cleanup in hyperopt
2019-08-02 20:02:46 +02:00
Matthias
32605fa10a small improvements 2019-08-02 19:52:56 +02:00
Matthias
0b9b5f3993 Improve document wording 2019-08-02 19:50:12 +02:00
Matthias
86aa18efe6 Merge pull request #2082 from freqtrade/fix/missintfstring
Fix/missintfstring
2019-08-02 10:27:10 +02:00
Matthias
76d22bc743 Show correct valueerror message 2019-08-02 09:41:24 +02:00
Matthias
01cd30984b Improve wording 2019-08-02 06:47:03 +02:00
Matthias
fceb411154 Create detailed section about strategy problem analysis 2019-08-02 06:44:31 +02:00
Jonathan Raviotta
0413598d7b adding environment.yml for conda builds 2019-08-01 19:30:45 -04:00
hroff-1902
3ccfe88ad8 tests adjusted 2019-08-01 23:57:50 +03:00
hroff-1902
065ebd39ef cleanup in hyperopt 2019-08-01 23:57:26 +03:00
Matthias
bcccdda7c0 Merge branch 'develop' into align_userdata 2019-08-01 19:33:45 +02:00
Matthias
4c005e7086 Merge pull request #2075 from hroff-1902/hyperopt-cleanup2
minor: hyperopt cleanups and output improvements
2019-08-01 07:08:50 +02:00
Matthias
2a141af42e Only create userdir when explicitly requested 2019-07-31 19:39:54 +02:00
Matthias
472690a55f Merge pull request #2073 from freqtrade/update/setuppy
Improve setup.py to allow "extras" installations
2019-07-31 19:25:21 +02:00
Matthias
8cef567abc create and use hyperopt-results folder 2019-07-31 07:10:17 +02:00
Matthias
5d22d541f2 Add forgotten directory 2019-07-31 06:58:26 +02:00
Matthias
c3d14ab9b9 don't use "folder" ... 2019-07-31 06:54:45 +02:00
Matthias
0488525888 Fix some documentation errors 2019-07-31 06:49:25 +02:00
Matthias
b8713a515e Merge pull request #2071 from freqtrade/new-dev
New develop version 2019.7-dev
2019-07-30 11:31:22 +02:00
hroff-1902
b976f24672 tests adjusted 2019-07-30 11:47:46 +03:00
hroff-1902
8f1f416a52 hyperopt cleanup and output improvements 2019-07-30 11:47:28 +03:00
Matthias
0d9d23a888 Merge pull request #2070 from freqtrade/new_release
New release 2019.7
2019-07-30 06:19:43 +02:00
Matthias
a5fb3e08f7 Merge pull request #2072 from freqtrade/improve_dev_docs
Improve release documentation
2019-07-30 06:12:47 +02:00
Matthias
59caff8fb1 UPdate developer docs 2019-07-29 20:57:57 +02:00
Matthias
f825e81d0e developers need all dependencies! 2019-07-29 20:54:35 +02:00
Matthias
7bea0007c7 Allow installing via submodules
freqtrade can be installed using `pip install -e .[all]` to include all
dependencies
2019-07-29 20:53:26 +02:00
Matthias
8dd8addd3a Sort requirements-dev file 2019-07-29 20:52:38 +02:00
Matthias
e14dd4974f Improve release documentation 2019-07-29 20:32:28 +02:00
Matthias
7a97995d81 2017.7-dev version bump 2019-07-29 20:30:14 +02:00
Matthias
e64509f1b4 Version bump to 2019.7 2019-07-29 20:27:50 +02:00
Matthias
0ac5440fc2 Merge pull request #2069 from freqtrade/pyup/scheduled-update-2019-07-29
Scheduled weekly dependency update for week 30
2019-07-29 20:07:44 +02:00
Matthias
fde3411c8b Merge branch 'develop' into pyup/scheduled-update-2019-07-29 2019-07-29 19:39:09 +02:00
Matthias
8066aba6fe Merge pull request #2044 from freqtrade/pyup/scheduled-update-2019-07-22
Scheduled weekly dependency update for week 29
2019-07-29 19:37:28 +02:00
pyup-bot
5ba0aa8082 Update plotly from 3.10.0 to 4.0.0 2019-07-29 15:25:16 +00:00
pyup-bot
3e95b7d8a5 Update mypy from 0.711 to 0.720 2019-07-29 15:25:15 +00:00
pyup-bot
0f632201e0 Update pytest from 5.0.0 to 5.0.1 2019-07-29 15:25:14 +00:00
pyup-bot
ebca1e4357 Update flake8 from 3.7.7 to 3.7.8 2019-07-29 15:25:12 +00:00
pyup-bot
a3620c60ad Update flask from 1.0.3 to 1.1.1 2019-07-29 15:25:11 +00:00
pyup-bot
9f70ebecf1 Update arrow from 0.14.2 to 0.14.3 2019-07-29 15:25:10 +00:00
pyup-bot
0fd91e4450 Update sqlalchemy from 1.3.5 to 1.3.6 2019-07-29 15:25:09 +00:00
pyup-bot
fe088dc8c3 Update ccxt from 1.18.860 to 1.18.992 2019-07-29 15:25:08 +00:00
pyup-bot
5a6e20a6aa Update pandas from 0.24.2 to 0.25.0 2019-07-29 15:25:07 +00:00
pyup-bot
02bfe2dad3 Update numpy from 1.16.4 to 1.17.0 2019-07-29 15:25:06 +00:00
Matthias
50edd4cfdd Merge pull request #2046 from freqtrade/pyup/fix_update_07_22
Pyup/fix update 07 22
2019-07-29 13:28:40 +02:00
Matthias
03e60b9ea4 Rename folder_Operations to directory_operations 2019-07-29 06:15:49 +02:00
Matthias
0677472c56 Merge pull request #2066 from freqtrade/hyperopt/tests
Fix some hyperopt tests
2019-07-28 19:33:18 +02:00
Matthias
c1bc1e3137 Add documentation for user_data_dir 2019-07-28 15:34:49 +02:00
Matthias
b691fb7f2d Fix some hyperopt tests 2019-07-28 15:19:17 +02:00
Matthias
73ac98da80 Small fixes while tsting 2019-07-28 15:11:41 +02:00
Matthias
14b43b504b Use user_data_dir for hyperopt 2019-07-28 15:05:17 +02:00
Matthias
a3c605f147 PairListResovler to use user_data_dir 2019-07-28 14:58:06 +02:00
Matthias
333413d298 Add default_conf to strategy tests 2019-07-28 14:58:06 +02:00
Matthias
9de8d7276e have strategyresolver use user_data_dir 2019-07-28 14:57:05 +02:00
Matthias
432b106d58 Improve docstring, remove unneeded method 2019-07-28 14:57:05 +02:00
Matthias
2c7a248307 Use user_data_dir in hyperopt 2019-07-28 14:57:05 +02:00
Matthias
113947132c user_data_dir is PATH in config, not str 2019-07-28 14:57:05 +02:00
Matthias
0a253d66d0 Remove os.path from hyperopt 2019-07-28 14:57:05 +02:00
Matthias
ae0e001187 Fix some bugs in tests 2019-07-28 14:57:05 +02:00
Matthias
eab82fdec7 plot-scripts use user_data_dir 2019-07-28 14:57:05 +02:00
Matthias
da755d1c83 Remove obsolete variable 2019-07-28 14:57:05 +02:00
Matthias
1b2581f0cb Add user_data_dir to configuration 2019-07-28 14:57:05 +02:00
Matthias
56c8bdbaa2 Test create-userdir command line option 2019-07-28 14:57:05 +02:00
Matthias
23435512c4 Add create-userdir command to initialize a user directory 2019-07-28 14:57:05 +02:00
Matthias
6c3a0eb1d6 add create_userdir function 2019-07-28 14:55:19 +02:00
Matthias
c85cd13ca1 Change default backtest result to "backtest_results" - backtest_data is
misleading
2019-07-28 14:55:19 +02:00
Matthias
e4b994381b Merge pull request #2060 from hroff-1902/improve-logging
Improve logging: output divider in logs between throttles
2019-07-28 14:45:16 +02:00
Matthias
de2a2473f5 Merge pull request #2050 from mrsegen/patch-1
Resolve issue #2042
2019-07-28 14:11:03 +02:00
Matthias
e6b036b413 Merge pull request #2064 from hroff-1902/remove-pytest-warning4
get rid of pandas warning in pytest
2019-07-28 14:10:16 +02:00
Leif Segen
08a3d26328 Update bot-usage.md
Update in response to feedback.
2019-07-27 18:35:21 -05:00
hroff-1902
bc299067aa get rid of pandas warning in pytest 2019-07-27 23:24:06 +03:00
Matthias
908a0277e5 Merge pull request #2062 from hroff-1902/remove-pytest-warning1
minor: eliminate warnings in pytest
2019-07-26 14:40:11 +02:00
hroff-1902
c2deb1db25 eliminate warnings in pytest when testing handling of the deprecated strategy interfaces 2019-07-26 14:23:00 +03:00
Matthias
16716ad028 Merge pull request #2057 from freqtrade/refactor/argument_location
Move argument definitions to their own file
2019-07-26 06:19:04 +02:00
Matthias
fef8fe8525 Merge pull request #2055 from freqtrade/get_order_exception
Get order exception
2019-07-26 06:17:15 +02:00
Matthias
3d5268368f Merge pull request #2059 from hroff-1902/docs-minor-fixes
Docs minor fixes
2019-07-26 06:08:09 +02:00
Matthias
20b51da180 Merge pull request #2056 from freqtrade/deprecate_live_bt
Deprecate live bt
2019-07-26 06:02:27 +02:00
hroff-1902
785a7a22bc output divider in logs between throttles 2019-07-26 04:02:34 +03:00
hroff-1902
1ac4a7e116 rendering for a Note fixed 2019-07-26 02:59:10 +03:00
hroff-1902
327e505273 non-working link to misc.py removed 2019-07-26 02:57:51 +03:00
hroff-1902
bf1c197a37 import errors fixed 2019-07-26 02:21:31 +03:00
Matthias
3c3a902a69 Move argument definitions to their own file 2019-07-25 20:42:08 +02:00
Matthias
0c14176cd7 Deprecate --live 2019-07-25 20:36:19 +02:00
Matthias
7ee971c3e3 Add simple method to add deprecations to cmd line options 2019-07-25 20:35:20 +02:00
Matthias
098a23adc6 Merge pull request #2048 from hroff-1902/hyperopt-loss-onlyprofit2
minor: add OnlyProfitHyperOptLoss
2019-07-25 20:18:05 +02:00
hroff-1902
10c69387fd docs adjusted 2019-07-25 21:07:17 +03:00
Matthias
4b8b2f7c5b Use raise xxx from e to have a nicer traceback 2019-07-25 20:06:20 +02:00
Matthias
e1b8ff798f Add test to verify that get_order was successfully cought 2019-07-25 20:05:48 +02:00
Matthias
05b1854946 Gracefully handle InvalidOrderException. 2019-07-25 19:56:59 +02:00
hroff-1902
f58668fd67 test added 2019-07-25 20:54:12 +03:00
Matthias
e8843c31e6 Merge pull request #2045 from hroff-1902/add-hyperopt-path
add --hyperopt-path option
2019-07-25 10:42:23 +02:00
hroff-1902
05be16e9e1 helpstring alignment fixed 2019-07-25 08:49:33 +03:00
hroff-1902
e9b77298a7 max() removed 2019-07-25 08:17:41 +03:00
Matthias
a0cecc6c52 Fix test after pandas 0.25.0 update 2019-07-24 06:29:50 +02:00
Leif Segen
cf6113068c Resolve issue #2042
Issue #2042 noted that the terminal output from `setup.sh` regarding an option use the bot was missing from the documentation. This has been added.
2019-07-23 22:52:42 -05:00
hroff-1902
0c2c094db6 minor: add OnlyProfitHyperOptLoss 2019-07-23 18:51:24 +03:00
Matthias
60cf56e235 Adapt tests to always provide message for ccxt exceptions
Changes introduced in https://github.com/ccxt/ccxt/pull/5470
2019-07-22 20:59:49 +02:00
Matthias
482f5f7a26 Update plotly dependencies (will break 3.x installations) 2019-07-22 20:39:38 +02:00
hroff-1902
04382d4b44 add --hyperopt-path option 2019-07-22 20:23:18 +03:00
pyup-bot
44b2261c34 Update plotly from 3.10.0 to 4.0.0 2019-07-22 15:23:13 +00:00
pyup-bot
76b9d781ee Update mypy from 0.711 to 0.720 2019-07-22 15:23:12 +00:00
pyup-bot
bd0faaf702 Update pytest from 5.0.0 to 5.0.1 2019-07-22 15:23:11 +00:00
pyup-bot
e0cd34c9e1 Update flake8 from 3.7.7 to 3.7.8 2019-07-22 15:23:09 +00:00
pyup-bot
6c41ca4b8c Update flask from 1.0.3 to 1.1.1 2019-07-22 15:23:08 +00:00
pyup-bot
7add015a75 Update sqlalchemy from 1.3.5 to 1.3.6 2019-07-22 15:23:07 +00:00
pyup-bot
d6b6e59ab8 Update ccxt from 1.18.860 to 1.18.965 2019-07-22 15:23:06 +00:00
pyup-bot
a213674a98 Update pandas from 0.24.2 to 0.25.0 2019-07-22 15:23:05 +00:00
Matthias
41f24898e5 Merge pull request #2043 from freqtrade/combine/resolvers
Combine/resolvers
2019-07-22 06:19:31 +02:00
Matthias
d2ad32eef8 partially revert last commit(DefaultStrategy import IS needed).
* don't run functions in travis in a way we don't support
2019-07-21 19:56:43 +02:00
Matthias
1fea6d394a Import DefaultStrategy from the correct file 2019-07-21 19:31:50 +02:00
Matthias
dcddfce5bc Fix small mistakes 2019-07-21 19:21:50 +02:00
Matthias
e6528be63d Config is not optional for hyperopt resolver 2019-07-21 16:20:45 +02:00
Matthias
08ca260e82 Simplify return valuef rom _load_object 2019-07-21 15:29:17 +02:00
Matthias
88eb93da52 Fix base64 strategy test to make sure strategy was loaded via base64 2019-07-21 15:16:19 +02:00
Matthias
b35efd96dc Extract load_object from multiple paths to iResolver 2019-07-21 15:03:12 +02:00
Matthias
89db5c6bab Extract strategy-specific stuff from search logic
will allow extracting all to IResolver
2019-07-21 14:52:59 +02:00
Matthias
790838d897 Merge pull request #2024 from freqtrade/custom_hyperopt_loss
Custom hyperopt loss function (and sharpe-ratio)
2019-07-20 12:48:26 +02:00
Matthias
4d0cf9ec8e Merge pull request #2033 from hroff-1902/remove-dynamic-whitelist-option
remove deprecated --dynamic-whitelist option
2019-07-19 06:38:54 +02:00
Matthias
299f673a8e Merge pull request #2029 from freqtrade/create_datadir_pathlib
[minor] Convert create_datadir to Pathlib
2019-07-19 06:36:11 +02:00
Matthias
fa8904978b Don't use --hyperopt-loss-class, but --hyperopt-loss instead 2019-07-19 06:31:49 +02:00
hroff-1902
4a144d1c18 docs: description for whitelist and blacklist fixed 2019-07-18 22:43:36 +03:00
Matthias
415c96204a Merge pull request #2035 from hroff-1902/cleanup-arguments
minor: cleanup Arguments
2019-07-18 20:56:51 +02:00
hroff-1902
7af24dc486 cleanup Arguments: name attrs and methods as non-public 2019-07-18 21:43:40 +03:00
Matthias
e01c0ab4d6 Improve doc wording 2019-07-18 20:02:28 +02:00
Matthias
8b4827ad85 Convert create_datadir to Pathlib 2019-07-18 19:48:19 +02:00
hroff-1902
43d5ec2d4a docs: removed historical excursus which can confuse new users 2019-07-18 18:15:51 +03:00
hroff-1902
75a0998ed2 docs: restore link to #dynamic-pairlists. 2019-07-18 18:08:02 +03:00
Matthias
fbd229810f Merge pull request #2034 from hroff-1902/option-version
minor: add -V alias for --version
2019-07-18 14:06:05 +02:00
Matthias
d27e791f32 Merge pull request #2031 from freqtrade/randomize_tests_again
Randomize tests again
2019-07-18 13:53:48 +02:00
hroff-1902
50d2950e6b add -V alias for --version 2019-07-18 12:12:34 +03:00
hroff-1902
96564d0dad remove deprecated --dynamic-whitelist option 2019-07-18 10:45:47 +03:00
Matthias
3e5abd18ca Randomize tests again
this used to be enabled, but the plugin changed how it works
> From v1.0.0 onwards, this plugin no longer randomises tests by default.
2019-07-18 06:56:52 +02:00
Matthias
545ff6f9f1 Fix typo 2019-07-18 06:31:44 +02:00
Matthias
49b95fe008 use Path.cwd() instead of odd parent.parent.parent structure 2019-07-17 20:52:17 +02:00
Matthias
b8704e12b7 Add sample hyperopt loss file 2019-07-17 20:51:44 +02:00
Matthias
639a4d5cf7 Allow importing interface from hyperopt.py 2019-07-17 07:15:43 +02:00
Matthias
0e500de1a0 Add sample loss and improve docstring 2019-07-17 06:32:24 +02:00
Matthias
c5b244419d Merge branch 'develop' into custom_hyperopt_loss 2019-07-17 06:27:42 +02:00
Matthias
8ccfc0f316 Remove unused variables 2019-07-17 06:24:40 +02:00
Matthias
e126c55a5a Merge pull request #2023 from hroff-1902/refactor/config3
minor: configuration cleanup
2019-07-17 06:20:21 +02:00
hroff-1902
be26ba8f8f rename _load_*_config() methods to _process_*_options() 2019-07-16 23:00:19 +03:00
Matthias
1493771087 improve description 2019-07-16 19:40:42 +02:00
Matthias
192d7ad735 Add column description to hyperopt documentation 2019-07-16 06:54:38 +02:00
Matthias
12679da5da Add test for hyperoptresolver 2019-07-16 06:50:25 +02:00
Matthias
ec49b22af3 Add sharpe ratio hyperopt loss 2019-07-16 06:45:13 +02:00
Matthias
d23179e25c Update hyperopt-loss to use resolver 2019-07-16 06:27:43 +02:00
Matthias
7d62bb8c53 Revert --clean argument to --continue 2019-07-16 05:51:26 +02:00
Matthias
c4e55d78d5 reword documentation 2019-07-16 05:41:39 +02:00
Matthias
07a1c48e8c Fix wrong intendation for custom-hyperopt check 2019-07-15 23:14:07 +02:00
Matthias
7be25313a5 Add some mypy ignores 2019-07-15 22:59:28 +02:00
Matthias
55e8092cbf Add sharpe ratio as loss function 2019-07-15 22:52:33 +02:00
Matthias
e5170582de Adapt tests to new loss-function method 2019-07-15 22:45:14 +02:00
Matthias
710443d200 Add documentation for custom hyperopt 2019-07-15 21:38:49 +02:00
Matthias
2a20423be6 Allow loading custom hyperopt loss functions 2019-07-15 21:35:42 +02:00
hroff-1902
8096a1fb04 minor: configuration cleanup 2019-07-15 22:17:57 +03:00
Matthias
2fedae6060 Move unnecessary things out of generate_optimizer 2019-07-15 20:31:55 +02:00
Matthias
b1b4048f97 Add test for hyperopt 2019-07-15 20:28:02 +02:00
Matthias
107f00ff8f Add hyperopt option to clean temporary pickle files 2019-07-15 20:17:15 +02:00
Matthias
5144e98a82 Merge pull request #2015 from hroff-1902/refactor/config2
Make configuration a module
2019-07-15 19:41:57 +02:00
Matthias
210d70b0c7 Merge pull request #2022 from freqtrade/fix/2020
Remove wrong import in legacy startup sript
2019-07-15 19:36:16 +02:00
Matthias
3ae94520c3 Merge pull request #2019 from freqtrade/small/cleanups
[Minor] Small code cleanups
2019-07-15 17:29:32 +02:00
Matthias
cbe25178d7 Merge pull request #2009 from hroff-1902/fix-2008
fix #2008
2019-07-15 10:55:33 +02:00
Matthias
a3b7e1f774 Update wording in docs 2019-07-15 06:59:20 +02:00
Matthias
bbab5fef0c Remove wrong import in legacy startup sript 2019-07-15 06:27:43 +02:00
hroff-1902
007703156b do not export ARGS_* from configuration 2019-07-15 01:55:35 +03:00
hroff-1902
9cae2900d4 get rid of patched_configuration_open() in tests 2019-07-15 01:44:25 +03:00
hroff-1902
876cae2807 docs adjusted to current default values; more detailed description of --eps and --dmmp added 2019-07-14 22:48:15 +03:00
Matthias
e955b1ae09 Use log_has_re instead of plain regex filters for log messages 2019-07-14 20:21:57 +02:00
Matthias
dadf8adb3e Replace filter usage 2019-07-14 20:14:35 +02:00
Matthias
4238ee090d Cleanup some code
after deepcode.ai suggestions
2019-07-14 20:05:28 +02:00
hroff-1902
65f77306d3 using logger.debug, info was too noisy 2019-07-14 21:00:48 +03:00
hroff-1902
efbc7cccb1 enable --dmmp for hyperopt 2019-07-14 20:56:17 +03:00
Matthias
f0206a90b1 Merge pull request #2018 from freqtrade/market_orders_with_price
Market orders with price
2019-07-14 19:29:44 +02:00
Matthias
a8f3f2bc1a Extend test to cover market orders with price too 2019-07-14 14:23:23 +02:00
Matthias
25822d1717 Add empty options dict to all tests using create_order 2019-07-14 14:18:30 +02:00
Matthias
9887cb997e Check if Price is needed for market orders
This is currently the case for:
cex, coinex, cointiger, fcoin, fcoinjp, hadax, huobipro, huobiru, uex,
2019-07-14 14:17:09 +02:00
Matthias
7e2be96516 Merge pull request #2017 from hroff-1902/resolver-filename
minor: improvements to resolvers
2019-07-14 13:37:00 +02:00
Matthias
2e1269c474 Revert comment for Exception that's not changed 2019-07-14 13:30:57 +02:00
hroff-1902
b499e74502 minor improvements to resolvers 2019-07-12 23:45:49 +03:00
Matthias
7536f6adbd Merge pull request #2004 from freqtrade/doc/starting
Don't run the bot with python3 freqtrade
2019-07-12 09:02:41 +02:00
Matthias
4be02bc207 Merge pull request #2014 from hroff-1902/fix-2013
Fix #2013
2019-07-12 08:14:46 +02:00
hroff-1902
bbfbd87a9f move create_datadir() to separate file 2019-07-12 03:31:36 +03:00
hroff-1902
7e103e34f8 flake happy 2019-07-12 01:41:09 +03:00
hroff-1902
94e6fb89b3 tests happy 2019-07-12 00:49:23 +03:00
hroff-1902
1bdffcc73b make configuration a sep. module, including arguments 2019-07-12 00:49:23 +03:00
hroff-1902
e993e010f4 Fix #2013 2019-07-11 23:02:57 +03:00
Matthias
bc1b5f477d Merge pull request #2010 from freqtrade/fix/docs
Fix non-rendering docs
2019-07-11 00:51:54 +02:00
Matthias
6a43128019 Fix non-rendering docs 2019-07-10 08:49:42 +02:00
hroff-1902
c474e2ac86 fix #2008 2019-07-10 01:53:40 +03:00
Matthias
7763b4cf5b Merge pull request #2007 from hroff-1902/fix-2005
fix #2005
2019-07-09 10:33:42 +02:00
hroff-1902
322227bf67 fix #2005 2019-07-09 00:59:34 +03:00
Matthias
27cb1a4174 Add FAQ section explaining "module not found" errors 2019-07-08 17:08:14 +02:00
Matthias
c4fb0fd6ca Don't run the bot with python3 freqtrade
* we can either use `python3 -m freqtrade ...` or `freqtrade ...` - and
shorter should be better.
2019-07-08 17:01:25 +02:00
Matthias
87ff1e8cb0 Merge pull request #2002 from hroff-1902/refactor/arguments2
minor: refactoring arguments and configuration
2019-07-08 16:56:25 +02:00
Matthias
61b24180f0 Merge pull request #1998 from freqtrade/fix/pax_balance
Support all types of pairs for /balance
2019-07-08 16:31:57 +02:00
hroff-1902
15d2cbd6df loggers: wording improved 2019-07-07 10:17:01 +03:00
hroff-1902
f7a2428deb max_open_trades may be -1 2019-07-07 10:13:00 +03:00
Matthias
6c2415d32f Rename parameters from pair to curr 2019-07-07 06:36:35 +02:00
hroff-1902
84d3868994 rename loglevel --> verbosity, because it's not logging level 2019-07-07 02:53:13 +03:00
hroff-1902
f89b2a18e0 fix loglevel in conftest -- it's actually the verbosity level 2019-07-07 02:42:03 +03:00
hroff-1902
8114d790a5 commit forgotten loggers.py 2019-07-07 01:40:52 +03:00
hroff-1902
082065cd50 minor cosmetics in arguments.py 2019-07-07 01:20:26 +03:00
hroff-1902
a65b5f8e02 make some more arguments positive integers 2019-07-07 01:10:41 +03:00
hroff-1902
d8f133aaf3 remove duplicated loglevel option 2019-07-07 00:51:01 +03:00
hroff-1902
8e272e5774 minor: cosmetics in arguments.py 2019-07-07 00:48:39 +03:00
hroff-1902
ce2a5b2838 move loggers setup out of configuration 2019-07-07 00:31:48 +03:00
Matthias
bcf2bc6f8c Merge pull request #1999 from freqtrade/minor/datadir
minor - Folders are not Directories
2019-07-04 20:25:44 +02:00
Matthias
17800c8ca5 Remove folder references (it's directory!) 2019-07-04 19:57:38 +02:00
Matthias
5c6039fd8b Fix #1997 - rename folder to dir 2019-07-04 19:53:50 +02:00
Matthias
40fe2d2c16 Test get_valid_pair_combination 2019-07-03 20:20:12 +02:00
Matthias
1bcf2737fe Add tests for new behaviour 2019-07-03 20:07:26 +02:00
Matthias
fcdbe846e5 Fix #1981 - Detect reverted currency pairs 2019-07-03 20:06:50 +02:00
Matthias
d055dc0c6e Merge pull request #1993 from freqtrade/refactor/arguments
Remove duplicate keyword from arguments
2019-07-03 12:01:41 +02:00
Matthias
e19c192570 Merge pull request #1994 from hroff-1902/fix-validate_timeframes
fix validate_timeframes()
2019-07-03 11:11:28 +02:00
hroff-1902
b80cef964e fix validate_timeframes(); test added 2019-07-03 11:18:39 +03:00
Matthias
b43594e4eb Merge pull request #1996 from hroff-1902/fix/1995
fix #1995
2019-07-03 06:44:23 +02:00
Matthias
0908863e07 Merge pull request #1987 from freqtrade/plot_script_changes
Plot script changes
2019-07-03 06:43:34 +02:00
Matthias
b3644f7fa0 Fix typo in docstring 2019-07-03 06:26:39 +02:00
hroff-1902
d41b8cc96e catch ccxt.BaseError 2019-07-03 05:13:41 +03:00
hroff-1902
91fb9d0113 fix #1995 2019-07-03 05:02:44 +03:00
Matthias
85ac217abc Remove duplicate keyword from arguments 2019-07-02 20:33:27 +02:00
Matthias
687381f42c Merge pull request #1991 from freqtrade/pyup/scheduled-update-2019-07-01
Scheduled weekly dependency update for week 26
2019-07-01 22:06:29 +02:00
pyup-bot
c91add203d Update mypy from 0.710 to 0.711 2019-07-01 18:28:32 +00:00
pyup-bot
1e4f459a26 Update pytest from 4.6.3 to 5.0.0 2019-07-01 18:28:31 +00:00
pyup-bot
06ad04e5fa Update ccxt from 1.18.805 to 1.18.860 2019-07-01 18:28:30 +00:00
Matthias
80bf5c9756 Merge pull request #1988 from freqtrade/fix/timeframes_crash
Gracefully fail on timeframes exception
2019-07-01 11:19:37 +02:00
Matthias
0d601fd111 Remove logger message 2019-07-01 06:18:28 +02:00
Matthias
01904d3c1e Test not having timeframe available on exchange object 2019-06-30 20:30:57 +02:00
Matthias
0c7d14fe50 Check if timeframes is available and fail gracefully otherwise 2019-06-30 20:30:31 +02:00
Matthias
cdeb649d0b Merge pull request #1967 from freqtrade/modify/setup.sh
Modify handling of pip in setup.sh
2019-06-30 19:52:50 +02:00
Matthias
79ae3c2f2e Merge pull request #1977 from hroff-1902/cleanup/freqtradebot
partial freqtradebot cleanup
2019-06-30 19:52:35 +02:00
Matthias
59818af69c Remove common_datearray function 2019-06-30 13:18:22 +02:00
Matthias
44e0500958 Test init_plotscript 2019-06-30 13:01:12 +02:00
Matthias
db59d39e2c Don't use class for plotting
This will allow easy usage of the methods from jupter notebooks
2019-06-30 11:08:02 +02:00
Matthias
587d71efb5 Test generate_profit_plot 2019-06-30 10:47:55 +02:00
Matthias
c7a4a16eec Create generate_plot_graph 2019-06-30 10:31:36 +02:00
Matthias
0b517584aa Use add_profit in script 2019-06-30 10:26:53 +02:00
Matthias
5a11ffcad8 Add test for add_profit 2019-06-30 10:24:10 +02:00
Matthias
0a184d380e create add_profit function 2019-06-30 10:14:33 +02:00
Matthias
6b387d320e extract combine_tickers to btanalysis 2019-06-30 10:04:43 +02:00
Matthias
348513c151 Improve formatting of plotting.py 2019-06-30 09:47:07 +02:00
Matthias
0d5e94b147 Rename generate_row to add_indicators 2019-06-30 09:44:50 +02:00
Matthias
88545d882c Use FTPlots class in plot-scripts 2019-06-30 09:42:10 +02:00
Matthias
42ea0a19d2 create FTPlots class to combine duplicate script code 2019-06-30 09:41:43 +02:00
Matthias
c87d27048b align plot_profit to plot_dataframe 2019-06-30 09:28:49 +02:00
Matthias
700bab7279 Rename generate_plot_file to store_plot_file 2019-06-30 09:28:34 +02:00
Matthias
c3db4ebbc3 Revise plot_profit to use pandas functions where possible 2019-06-29 20:52:33 +02:00
Matthias
8aa327cb8a Add load_trades abstraction (to load trades from either DB or file) 2019-06-29 20:52:23 +02:00
Matthias
4218d569de Only read trades once 2019-06-29 20:41:22 +02:00
Matthias
e50eee59cf Seperate plot-name generation and plotting 2019-06-29 20:38:49 +02:00
Matthias
4506832925 Update docstring 2019-06-29 20:07:25 +02:00
Matthias
a0cdc63a5d Merge pull request #1984 from asmodehn/bitstamp_bad
adding bitstamp to list of bad exchanges.
2019-06-29 19:51:01 +02:00
Matthias
79b4e2dc85 Rename generate_graph to generate_candlestick_graph 2019-06-29 17:23:33 +02:00
Matthias
edd3fc8825 Add test for create_cum_profit 2019-06-29 17:22:47 +02:00
AlexV
e8796e009c adding bitstamp to list of bad exchanges. 2019-06-29 17:20:10 +02:00
Matthias
044be3b93e Add create_cum_profit column 2019-06-29 16:57:04 +02:00
Matthias
0436811cf0 Use mode OTHER, nto backtesting 2019-06-28 06:47:40 +02:00
Matthias
152e138c17 Merge pull request #1979 from hroff-1902/fix/1978
fix #1978
2019-06-28 06:04:32 +02:00
hroff-1902
4f5e212f87 fix #1978 2019-06-28 01:01:51 +03:00
hroff-1902
21bf01a24c partial freqtradebot cleanup 2019-06-27 22:29:17 +03:00
Matthias
16a9e6b72f Improve install documentation 2019-06-27 19:51:04 +02:00
Matthias
700bc087d3 Merge pull request #1952 from hroff-1902/fix/1948
Fix #1948
2019-06-27 19:36:06 +02:00
Matthias
8b99348e98 Merge pull request #1975 from freqtrade/fix/dry_run_bal
Show different message for balance during dry-run
2019-06-27 19:34:51 +02:00
Matthias
045f34e851 Merge pull request #1974 from hroff-1902/fix/1963
fix #1963
2019-06-27 19:34:17 +02:00
hroff-1902
e5a8030dd7 comment added 2019-06-27 16:42:10 +03:00
Matthias
6643b83afe Update tests to test both balance versions 2019-06-27 07:06:35 +02:00
Matthias
98681b78b4 Show ifferent message for balance in dry-run 2019-06-27 07:06:11 +02:00
Matthias
f8dd0b0cb3 Use parenteses instead of \ seperators 2019-06-27 06:32:26 +02:00
Matthias
f04d49886b Add test to verify behaviour if currency in fee-dict is None 2019-06-27 06:29:18 +02:00
Matthias
3043a8d9c9 Be more explicit about what's missing 2019-06-27 06:20:22 +02:00
Matthias
4459fdf1b1 Merge pull request #1961 from freqtrade/feat/config_refactor
Argument handling refactor
2019-06-27 06:06:23 +02:00
Matthias
086d690df7 Merge pull request #1973 from hroff-1902/minor-typos-1
minor: couple of typos fixed
2019-06-27 05:49:58 +02:00
hroff-1902
05d93cda16 fix #1963 2019-06-27 01:03:38 +03:00
hroff-1902
6fc6eaf742 minor: couple of typos fixed 2019-06-26 22:23:16 +03:00
Matthias
596cee2dc1 Merge pull request #1972 from freqtrade/update_qtpylib
Update qtpylib from source
2019-06-26 20:34:28 +02:00
Matthias
1d5c3f34ae Update qtpylib from source 2019-06-26 20:00:16 +02:00
Matthias
ca7080c2bb Merge pull request #1958 from freqtrade/new_release_dev
Version bump develop
2019-06-26 06:11:00 +02:00
Matthias
a89112a133 Merge pull request #1969 from freqtrade/developer_doc_improve
[minor] Improve developer-document
2019-06-25 07:04:06 +02:00
Matthias
353437bbd1 07 is July!! 2019-06-24 21:08:40 +02:00
Matthias
8e92fc62a3 Use correct new versioning now 2019-06-24 20:18:06 +02:00
Matthias
c106534663 Improve developer-document
to include a note to keep both branches uptodate while creating a changelog.

Cost me ~5 minutes doing the 2019.6 release...
2019-06-24 20:13:40 +02:00
Matthias
b92c6cdf35 Cleanup arguments and test_arguments 2019-06-24 20:10:50 +02:00
Matthias
ca5093901b Use build_args for plot script 2019-06-24 20:08:17 +02:00
Matthias
ba7a0dde06 Use build_args for download script 2019-06-24 20:08:17 +02:00
Matthias
27798c1683 Remove main_options 2019-06-24 20:08:15 +02:00
Matthias
ee312ac230 Use build_args for plot_dataframe script 2019-06-24 20:07:04 +02:00
Matthias
7e82be53cd Use build_args to build subcomand arguments 2019-06-24 20:05:17 +02:00
Matthias
7017e46ba1 Add dict with all possible cli arguments 2019-06-24 20:05:13 +02:00
Matthias
7166674d6c Move check_int_positive out of arguments class 2019-06-24 19:55:16 +02:00
Matthias
e1daf02735 UPdate version for develop 2019-06-24 19:46:39 +02:00
Matthias
1b156e0f34 Don't install python to a system, it's error-prone and may not work 2019-06-24 07:10:24 +02:00
Matthias
c1ee5d69c9 Try to get travis cache to work correctly 2019-06-24 07:09:54 +02:00
hroff-1902
7fbdf36c64 avoid code duplication while selecting min_roi entries 2019-06-23 19:23:51 +03:00
hroff-1902
144e053a4e fix for #1948 2019-06-20 03:26:25 +03:00
hroff-1902
a8efb1e1c8 test for #1948 added 2019-06-20 03:26:02 +03:00
118 changed files with 6422 additions and 3691 deletions

17
.dependabot/config.yml Normal file
View File

@@ -0,0 +1,17 @@
version: 1
update_configs:
- package_manager: "python"
directory: "/"
update_schedule: "weekly"
allowed_updates:
- match:
update_type: "all"
target_branch: "develop"
- package_manager: "docker"
directory: "/"
update_schedule: "daily"
allowed_updates:
- match:
update_type: "all"

View File

@@ -5,6 +5,7 @@ If it hasn't been reported, please create a new issue.
## Step 2: Describe your environment ## Step 2: Describe your environment
* Operating system: ____
* Python Version: _____ (`python -V`) * Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`) * CCXT version: _____ (`pip freeze | grep ccxt`)
* Branch: Master | Develop * Branch: Master | Develop

11
.gitignore vendored
View File

@@ -6,7 +6,10 @@ config*.json
.hyperopt .hyperopt
logfile.txt logfile.txt
hyperopt_trials.pickle hyperopt_trials.pickle
user_data/ user_data/*
!user_data/notebooks
user_data/notebooks/*
!user_data/notebooks/*example.ipynb
freqtrade-plot.html freqtrade-plot.html
freqtrade-profit-plot.html freqtrade-profit-plot.html
@@ -80,8 +83,7 @@ docs/_build/
target/ target/
# Jupyter Notebook # Jupyter Notebook
.ipynb_checkpoints *.ipynb_checkpoints
*.ipynb
# pyenv # pyenv
.python-version .python-version
@@ -93,3 +95,6 @@ target/
.pytest_cache/ .pytest_cache/
.mypy_cache/ .mypy_cache/
#exceptions
!*.gitkeep

View File

@@ -1,37 +0,0 @@
# autogenerated pyup.io config file
# see https://pyup.io/docs/configuration/ for all available options
# configure updates globally
# default: all
# allowed: all, insecure, False
update: all
# configure dependency pinning globally
# default: True
# allowed: True, False
pin: True
# update schedule
# default: empty
# allowed: "every day", "every week", ..
schedule: "every week"
search: False
# Specify requirement files by hand, default is empty
# default: empty
# allowed: list
requirements:
- requirements.txt
- requirements-dev.txt
- requirements-plot.txt
- requirements-common.txt
# configure the branch prefix the bot is using
# default: pyup-
branch_prefix: pyup/
# allow to close stale PRs
# default: True
close_prs: True

View File

@@ -10,16 +10,11 @@ services:
env: env:
global: global:
- IMAGE_NAME=freqtradeorg/freqtrade - IMAGE_NAME=freqtradeorg/freqtrade
addons:
apt:
packages:
- libelf-dev
- libdw-dev
- binutils-dev
install: install:
- cd build_helpers && ./install_ta-lib.sh; cd .. - cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd ..
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH
- pip install --upgrade pytest-random-order - export TA_LIBRARY_PATH=${HOME}/dependencies/lib
- export TA_INCLUDE_PATH=${HOME}/dependencies/lib/include
- pip install -r requirements-dev.txt - pip install -r requirements-dev.txt
- pip install -e . - pip install -e .
jobs: jobs:
@@ -27,20 +22,25 @@ jobs:
include: include:
- stage: tests - stage: tests
script: script:
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ - pytest --random-order --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
# Allow failure for coveralls # Allow failure for coveralls
- coveralls || true - coveralls || true
name: pytest name: pytest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- python freqtrade --datadir freqtrade/tests/testdata backtesting - freqtrade --datadir freqtrade/tests/testdata backtesting
name: backtest name: backtest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- python freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 - freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5
name: hyperopt name: hyperopt
- script: flake8 freqtrade scripts - script: flake8 freqtrade scripts
name: flake8 name: flake8
- script:
# Test Documentation boxes -
# !!! <TYPE>: is not allowed!
- grep -Er '^!{3}\s\S+:' docs/*; test $? -ne 0
name: doc syntax
- script: mypy freqtrade scripts - script: mypy freqtrade scripts
name: mypy name: mypy
@@ -56,4 +56,4 @@ notifications:
cache: cache:
pip: True pip: True
directories: directories:
- /usr/local/lib - $HOME/dependencies

View File

@@ -1,4 +1,4 @@
FROM python:3.7.3-slim-stretch FROM python:3.7.4-slim-stretch
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev \ && apt-get -y install curl build-essential libssl-dev \

View File

@@ -3,9 +3,7 @@
import sys import sys
import warnings import warnings
from freqtrade.main import main, set_loggers from freqtrade.main import main
set_loggers()
warnings.warn( warnings.warn(
"Deprecated - To continue to run the bot like this, please run `pip install -e .` again.", "Deprecated - To continue to run the bot like this, please run `pip install -e .` again.",

View File

@@ -1,8 +1,14 @@
if [ ! -f "/usr/local/lib/libta_lib.a" ]; then if [ -z "$1" ]; then
INSTALL_LOC=/usr/local
else
INSTALL_LOC=${1}
fi
echo "Installing to ${INSTALL_LOC}"
if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
tar zxvf ta-lib-0.4.0-src.tar.gz tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib \ cd ta-lib \
&& sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \
&& ./configure \ && ./configure --prefix=${INSTALL_LOC}/ \
&& make \ && make \
&& which sudo && sudo make install || make install \ && which sudo && sudo make install || make install \
&& cd .. && cd ..

View File

@@ -123,5 +123,5 @@
"process_throttle_secs": 5 "process_throttle_secs": 5
}, },
"strategy": "DefaultStrategy", "strategy": "DefaultStrategy",
"strategy_path": "/some/folder/" "strategy_path": "user_data/strategies/"
} }

View File

@@ -3,9 +3,43 @@
This page explains how to validate your strategy performance by using This page explains how to validate your strategy performance by using
Backtesting. Backtesting.
## Getting data for backtesting and hyperopt
To download data (candles / OHLCV) needed for backtesting and hyperoptimization use the `freqtrade download-data` command.
If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes.
Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory.
Alternatively, a `pairs.json` file can be used.
If you are using Binance for example:
- create a directory `user_data/data/binance` and copy `pairs.json` in that directory.
- update the `pairs.json` to contain the currency pairs you are interested in.
```bash
mkdir -p user_data/data/binance
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
```
Then run:
```bash
freqtrade download-data --exchange binance
```
This will download ticker data for all the currency pairs you defined in `pairs.json`.
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
- To change the exchange used to download the tickers, please use a different configuration file (you'll probably need to adjust ratelimits etc.)
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
- To download ticker data for only 10 days, use `--days 10` (defaults to 30 days).
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
## Test your strategy with Backtesting ## Test your strategy with Backtesting
Now you have good Buy and Sell strategies, you want to test it against Now you have good Buy and Sell strategies and some historic data, you want to test it against
real data. This is what we call real data. This is what we call
[backtesting](https://en.wikipedia.org/wiki/Backtesting). [backtesting](https://en.wikipedia.org/wiki/Backtesting).
@@ -13,7 +47,7 @@ Backtesting will use the crypto-currencies (pair) from your config file
and load static tickers located in and load static tickers located in
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata). [/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
If the 5 min and 1 min ticker for the crypto-currencies to test is not If the 5 min and 1 min ticker for the crypto-currencies to test is not
already in the `testdata` folder, backtesting will download them already in the `testdata` directory, backtesting will download them
automatically. Testdata files will not be updated until you specify it. automatically. Testdata files will not be updated until you specify it.
The result of backtesting will confirm you if your bot has better odds of making a profit than a loss. The result of backtesting will confirm you if your bot has better odds of making a profit than a loss.
@@ -24,53 +58,52 @@ The backtesting is very easy with freqtrade.
#### With 5 min tickers (Per default) #### With 5 min tickers (Per default)
```bash ```bash
python3 freqtrade backtesting freqtrade backtesting
``` ```
#### With 1 min tickers #### With 1 min tickers
```bash ```bash
python3 freqtrade backtesting --ticker-interval 1m freqtrade backtesting --ticker-interval 1m
```
#### Update cached pairs with the latest data
```bash
python3 freqtrade backtesting --refresh-pairs-cached
```
#### With live data (do not alter your testdata files)
```bash
python3 freqtrade backtesting --live
``` ```
#### Using a different on-disk ticker-data source #### Using a different on-disk ticker-data source
Assume you downloaded the history data from the Bittrex exchange and kept it in the `user_data/data/bittrex-20180101` directory.
You can then use this data for backtesting as follows:
```bash ```bash
python3 freqtrade backtesting --datadir freqtrade/tests/testdata-20180101 freqtrade backtesting --datadir user_data/data/bittrex-20180101
``` ```
#### With a (custom) strategy file #### With a (custom) strategy file
```bash ```bash
python3 freqtrade -s TestStrategy backtesting freqtrade -s TestStrategy backtesting
``` ```
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory.
#### Comparing multiple Strategies
```bash
freqtrade backtesting --strategy-list TestStrategy1 AwesomeStrategy --ticker-interval 5m
```
Where `TestStrategy1` and `AwesomeStrategy` refer to class names of strategies.
#### Exporting trades to file #### Exporting trades to file
```bash ```bash
python3 freqtrade backtesting --export trades freqtrade backtesting --export trades
``` ```
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts folder. The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory.
#### Exporting trades to file specifying a custom filename #### Exporting trades to file specifying a custom filename
```bash ```bash
python3 freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json
``` ```
#### Running backtest with smaller testset #### Running backtest with smaller testset
@@ -81,7 +114,7 @@ you want to use. The last N ticks/timeframes will be used.
Example: Example:
```bash ```bash
python3 freqtrade backtesting --timerange=-200 freqtrade backtesting --timerange=-200
``` ```
#### Advanced use of timerange #### Advanced use of timerange
@@ -101,37 +134,6 @@ The full timerange specification:
- Use tickframes between POSIX timestamps 1527595200 1527618600: - Use tickframes between POSIX timestamps 1527595200 1527618600:
`--timerange=1527595200-1527618600` `--timerange=1527595200-1527618600`
#### Downloading new set of ticker data
To download new set of backtesting ticker data, you can use a download script.
If you are using Binance for example:
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
- update the `pairs.json` to contain the currency pairs you are interested in.
```bash
mkdir -p user_data/data/binance
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
```
Then run:
```bash
python scripts/download_backtest_data.py --exchange binance
```
This will download ticker data for all the currency pairs you defined in `pairs.json`.
- To use a different folder than the exchange specific default, use `--datadir user_data/data/some_directory`.
- To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`.
- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`.
- To download ticker data for only 10 days, use `--days 10`.
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with other options.
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
## Understand the backtesting result ## Understand the backtesting result
The most important in the backtesting is to understand the result. The most important in the backtesting is to understand the result.
@@ -231,13 +233,13 @@ To backtest multiple strategies, a list of Strategies can be provided.
This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple
strategies you'd like to compare, this should give a nice runtime boost. strategies you'd like to compare, this should give a nice runtime boost.
All listed Strategies need to be in the same folder. All listed Strategies need to be in the same directory.
``` bash ``` bash
freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades
``` ```
This will save the results to `user_data/backtest_data/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename. This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table). There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
Detailed output for all strategies one after the other will be available, so make sure to scroll up. Detailed output for all strategies one after the other will be available, so make sure to scroll up.

View File

@@ -2,62 +2,70 @@
This page explains the different parameters of the bot and how to run it. This page explains the different parameters of the bot and how to run it.
!!! Note
If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands.
## Bot commands ## Bot commands
``` ```
usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]] [--userdir PATH] [-s NAME] [--strategy-path PATH]
[--db-url PATH] [--sd-notify] [--db-url PATH] [--sd-notify]
{backtesting,edge,hyperopt} ... {backtesting,edge,hyperopt,create-userdir,list-exchanges} ...
Free, open source crypto trading bot Free, open source crypto trading bot
positional arguments: positional arguments:
{backtesting,edge,hyperopt} {backtesting,edge,hyperopt,create-userdir,list-exchanges}
backtesting Backtesting module. backtesting Backtesting module.
edge Edge module. edge Edge module.
hyperopt Hyperopt module. hyperopt Hyperopt module.
create-userdir Create user-data directory.
list-exchanges Print available exchanges.
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages). -v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified --logfile FILE Log to the file specified.
--version show program's version number and exit -V, --version show program's version number and exit
-c PATH, --config PATH -c PATH, --config PATH
Specify configuration file (default: None). Multiple Specify configuration file (default: `config.json`).
--config options may be used. Can be set to '-' to Multiple --config options may be used. Can be set to
read config from stdin. `-` to read config from stdin.
-d PATH, --datadir PATH -d PATH, --datadir PATH
Path to backtest data. Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
-s NAME, --strategy NAME -s NAME, --strategy NAME
Specify strategy class name (default: Specify strategy class name (default:
DefaultStrategy). `DefaultStrategy`).
--strategy-path PATH Specify additional strategy lookup path. --strategy-path PATH Specify additional strategy lookup path.
--dynamic-whitelist [INT] --db-url PATH Override trades database URL, this is useful in custom
Dynamically generate and update whitelist based on 24h deployments (default: `sqlite:///tradesv3.sqlite` for
BaseVolume (default: 20). DEPRECATED. Live Run mode, `sqlite://` for Dry Run).
--db-url PATH Override trades database URL, this is useful if
dry_run is enabled or in custom deployments (default:
None).
--sd-notify Notify systemd service manager. --sd-notify Notify systemd service manager.
``` ```
### How to use a different configuration file? ### How to specify which configuration file be used?
The bot allows you to select which configuration file you want to use. Per The bot allows you to select which configuration file you want to use by means of
default, the bot will load the file `./config.json` the `-c/--config` command line option:
```bash ```bash
python3 freqtrade -c path/far/far/away/config.json freqtrade -c path/far/far/away/config.json
``` ```
Per default, the bot loads the `config.json` configuration file from the current
working directory.
### How to use multiple configuration files? ### How to use multiple configuration files?
The bot allows you to use multiple configuration files by specifying multiple The bot allows you to use multiple configuration files by specifying multiple
`-c/--config` configuration options in the command line. Configuration parameters `-c/--config` options in the command line. Configuration parameters
defined in the last configuration file override parameters with the same name defined in the latter configuration files override parameters with the same name
defined in the previous configuration file specified in the command line. defined in the previous configuration files specified in the command line earlier.
For example, you can make a separate configuration file with your key and secrete For example, you can make a separate configuration file with your key and secrete
for the Exchange you use for trading, specify default configuration file with for the Exchange you use for trading, specify default configuration file with
@@ -65,13 +73,13 @@ empty key and secrete values while running in the Dry Mode (which does not actua
require them): require them):
```bash ```bash
python3 freqtrade -c ./config.json freqtrade -c ./config.json
``` ```
and specify both configuration files when running in the normal Live Trade Mode: and specify both configuration files when running in the normal Live Trade Mode:
```bash ```bash
python3 freqtrade -c ./config.json -c path/to/secrets/keys.config.json freqtrade -c ./config.json -c path/to/secrets/keys.config.json
``` ```
This could help you hide your private Exchange key and Exchange secrete on you local machine This could help you hide your private Exchange key and Exchange secrete on you local machine
@@ -82,6 +90,29 @@ of your configuration in the project issues or in the Internet.
See more details on this technique with examples in the documentation page on See more details on this technique with examples in the documentation page on
[configuration](configuration.md). [configuration](configuration.md).
### Where to store custom data
Freqtrade allows the creation of a user-data directory using `freqtrade create-userdir --userdir someDirectory`.
This directory will look as follows:
```
user_data/
├── backtest_results
├── data
├── hyperopts
├── hyperopts_results
├── plot
└── strategies
```
You can add the entry "user_data_dir" setting to your configuration, to always point your bot to this directory.
Alternatively, pass in `--userdir` to every command.
The bot will fail to start if the directory does not exist, but will create necessary subdirectories.
This directory should contain your custom strategies, custom hyperopts and hyperopt loss functions, backtesting historical data (downloaded using either backtesting command or the download script) and plot outputs.
It is recommended to use version control to keep track of changes to your strategies.
### How to use **--strategy**? ### How to use **--strategy**?
This parameter will allow you to load your custom strategy class. This parameter will allow you to load your custom strategy class.
@@ -97,7 +128,7 @@ In `user_data/strategies` you have a file `my_awesome_strategy.py` which has
a strategy class called `AwesomeStrategy` to load it: a strategy class called `AwesomeStrategy` to load it:
```bash ```bash
python3 freqtrade --strategy AwesomeStrategy freqtrade --strategy AwesomeStrategy
``` ```
If the bot does not find your strategy file, it will display in an error If the bot does not find your strategy file, it will display in an error
@@ -109,27 +140,17 @@ Learn more about strategy file in
### How to use **--strategy-path**? ### How to use **--strategy-path**?
This parameter allows you to add an additional strategy lookup path, which gets This parameter allows you to add an additional strategy lookup path, which gets
checked before the default locations (The passed path must be a folder!): checked before the default locations (The passed path must be a directory!):
```bash ```bash
python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
#### How to install a strategy? #### How to install a strategy?
This is very simple. Copy paste your strategy file into the folder This is very simple. Copy paste your strategy file into the directory
`user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it.
### How to use **--dynamic-whitelist**?
!!! danger "DEPRECATED"
This command line option is deprecated. Please move your configurations using it
to the configurations that utilize the `StaticPairList` or `VolumePairList` methods set
in the configuration file
as outlined [here](configuration/#dynamic-pairlists)
Description of this deprecated feature was moved to [here](deprecated.md).
Please no longer use it.
### How to use **--db-url**? ### How to use **--db-url**?
When you run the bot in Dry-run mode, per default no transactions are When you run the bot in Dry-run mode, per default no transactions are
@@ -138,7 +159,7 @@ using `--db-url`. This can also be used to specify a custom database
in production mode. Example command: in production mode. Example command:
```bash ```bash
python3 freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite freqtrade -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
``` ```
## Backtesting commands ## Backtesting commands
@@ -174,9 +195,8 @@ optional arguments:
Disable applying `max_open_trades` during backtest Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high (same as setting `max_open_trades` to a very high
number). number).
-l, --live Use live data.
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...] --strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a commaseparated list of strategies to Provide a space-separated list of strategies to
backtest Please note that ticker-interval needs to be backtest Please note that ticker-interval needs to be
set either in config or via command line. When using set either in config or via command line. When using
this together with --export trades, the strategy-name this together with --export trades, the strategy-name
@@ -187,24 +207,16 @@ optional arguments:
--export-filename PATH --export-filename PATH
Save backtest results to this filename requires Save backtest results to this filename requires
--export to be set as well Example --export- --export to be set as well Example --export-
filename=user_data/backtest_data/backtest_today.json filename=user_data/backtest_results/backtest_today.json
(default: user_data/backtest_data/backtest- (default: user_data/backtest_results/backtest-
result.json) result.json)
``` ```
### How to use **--refresh-pairs-cached** parameter? ### Getting historic data for backtesting
The first time your run Backtesting, it will take the pairs you have The first time your run Backtesting, you will need to download some historic data first.
set in your config file and download data from the Exchange. This can be accomplished by using `freqtrade download-data`.
Check the corresponding [help page section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) for more details
If for any reason you want to update your data set, you use
`--refresh-pairs-cached` to force Backtesting to update the data it has.
!!! Note
Use it only if you want to update your data set. You will not be able to come back to the previous version.
To test your strategy with latest data, we recommend continuing using
the parameter `-l` or `--live`.
## Hyperopt commands ## Hyperopt commands
@@ -213,19 +225,23 @@ to find optimal parameter values for your stategy.
``` ```
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades MAX_OPEN_TRADES] [--max_open_trades INT]
[--stake_amount STAKE_AMOUNT] [-r] [--stake_amount STAKE_AMOUNT] [-r]
[--customhyperopt NAME] [--eps] [--dmmp] [-e INT] [--customhyperopt NAME] [--hyperopt-path PATH]
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [--eps] [-e INT]
[--print-all] [-j JOBS] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
[--dmmp] [--print-all] [--no-color] [-j JOBS]
[--random-state INT] [--min-trades INT] [--continue]
[--hyperopt-loss NAME]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
Specify ticker interval (1m, 5m, 30m, 1h, 1d). Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--max_open_trades MAX_OPEN_TRADES --max_open_trades INT
Specify max_open_trades to use. Specify max_open_trades to use.
--stake_amount STAKE_AMOUNT --stake_amount STAKE_AMOUNT
Specify stake_amount. Specify stake_amount.
@@ -235,30 +251,48 @@ optional arguments:
run your optimization commands with up-to-date data. run your optimization commands with up-to-date data.
--customhyperopt NAME --customhyperopt NAME
Specify hyperopt class name (default: Specify hyperopt class name (default:
DefaultHyperOpts). `DefaultHyperOpts`).
--hyperopt-path PATH Specify additional lookup path for Hyperopts and
Hyperopt Loss functions.
--eps, --enable-position-stacking --eps, --enable-position-stacking
Allow buying the same pair multiple times (position Allow buying the same pair multiple times (position
stacking). stacking).
-e INT, --epochs INT Specify number of epochs (default: 100).
-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]
Specify which parameters to hyperopt. Space-separated
list. Default: `all`.
--dmmp, --disable-max-market-positions --dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high (same as setting `max_open_trades` to a very high
number). number).
-e INT, --epochs INT Specify number of epochs (default: 100).
-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...], --spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]
Specify which parameters to hyperopt. Space separate
list. Default: all.
--print-all Print all results, not only the best ones. --print-all Print all results, not only the best ones.
--no-color Disable colorization of hyperopt results. May be
useful if you are redirecting output to a file.
-j JOBS, --job-workers JOBS -j JOBS, --job-workers JOBS
The number of concurrently running jobs for The number of concurrently running jobs for
hyperoptimization (hyperopt worker processes). If -1 hyperoptimization (hyperopt worker processes). If -1
(default), all CPUs are used, for -2, all CPUs but one (default), all CPUs are used, for -2, all CPUs but one
are used, etc. If 1 is given, no parallel computing are used, etc. If 1 is given, no parallel computing
code is used at all. code is used at all.
--random-state INT Set random state to some positive integer for
reproducible hyperopt results.
--min-trades INT Set minimal desired number of trades for evaluations
in the hyperopt optimization path (default: 1).
--continue Continue hyperopt from previous runs. By default,
temporary files will be removed and hyperopt will
start from scratch.
--hyperopt-loss NAME Specify the class name of the hyperopt loss function
class (IHyperOptLoss). Different functions can
generate completely different results, since the
target for optimization is different. Built-in
Hyperopt-loss-functions are: DefaultHyperOptLoss,
OnlyProfitHyperOptLoss, SharpeHyperOptLoss.
(default: `DefaultHyperOptLoss`).
``` ```
## Edge commands ## Edge commands
To know your trade expectacny and winrate against historical data, you can use Edge. To know your trade expectancy and winrate against historical data, you can use Edge.
``` ```
usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
@@ -289,11 +323,6 @@ optional arguments:
To understand edge and how to read the results, please read the [edge documentation](edge.md). To understand edge and how to read the results, please read the [edge documentation](edge.md).
## A parameter missing in the configuration?
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
## Next step ## Next step
The optimal strategy of the bot will change with time depending of the market trends. The next step is to The optimal strategy of the bot will change with time depending of the market trends. The next step is to

View File

@@ -1,15 +1,34 @@
# Configure the bot # Configure the bot
This page explains how to configure your `config.json` file. This page explains how to configure the bot.
## Setup config.json ## The Freqtrade configuration file
We recommend to copy and use the `config.json.example` as a template The bot uses a set of configuration parameters during its operation that all together conform the bot configuration. It normally reads its configuration from a file (Freqtrade configuration file).
Per default, the bot loads configuration from the `config.json` file located in the current working directory.
You can change the name of the configuration file used by the bot with the `-c/--config` command line option.
In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
If you used the [Quick start](installation.md/#quick-start) method for installing
the bot, the installation script should have already created the default configuration file (`config.json`) for you.
If default configuration file is not created we recommend you to copy and use the `config.json.example` as a template
for your bot configuration. for your bot configuration.
The table below will list all configuration parameters. The Freqtrade configuration file is to be written in the JSON format.
Mandatory Parameters are marked as **Required**. Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters.
Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates syntax of the configuration file at startup and will warn you if you made any errors editing it.
## Configuration parameters
The table below will list all configuration parameters available.
Mandatory parameters are marked as **Required**.
| Command | Default | Description | | Command | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
@@ -44,8 +63,8 @@ Mandatory Parameters are marked as **Required**.
| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode.
| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode.
| `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. | `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)).
| `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. | `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)).
| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
| `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
| `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded.
@@ -53,7 +72,8 @@ Mandatory Parameters are marked as **Required**.
| `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). | `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
| `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). | `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
| `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). | `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
| `pairlist.method` | StaticPairList | Use Static whitelist. [More information below](#dynamic-pairlists). | `experimental.block_bad_exchanges` | true | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists).
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). | `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
@@ -67,10 +87,11 @@ Mandatory Parameters are marked as **Required**.
| `initial_state` | running | Defines the initial application state. More information below. | `initial_state` | running | Defines the initial application state. More information below.
| `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below. | `forcebuy_enable` | false | Enables the RPC Commands to force a buy. More information below.
| `strategy` | DefaultStrategy | Defines Strategy class to use. | `strategy` | DefaultStrategy | Defines Strategy class to use.
| `strategy_path` | null | Adds an additional strategy lookup path (must be a folder). | `strategy_path` | null | Adds an additional strategy lookup path (must be a directory).
| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
| `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. | `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file.
| `user_data_dir` | cwd()/user_data | Directory containing user data. Defaults to `./user_data/`.
### Parameters in the strategy ### Parameters in the strategy
@@ -380,8 +401,6 @@ section of the configuration.
* `StaticPairList` * `StaticPairList`
* It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. * It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`.
* `VolumePairList` * `VolumePairList`
* Formerly available as `--dynamic-whitelist [<number_assets>]`. This command line
option is deprecated and should no longer be used.
* It selects `number_assets` top pairs based on `sort_key`, which can be one of * It selects `number_assets` top pairs based on `sort_key`, which can be one of
`askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`.
* There is a possibility to filter low-value coins that would not allow setting a stop loss * There is a possibility to filter low-value coins that would not allow setting a stop loss

View File

@@ -1,42 +1,202 @@
# Analyzing bot data # Analyzing bot data with Jupyter notebooks
After performing backtests, or after running the bot for some time, it will be interesting to analyze the results your bot generated. You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/`.
A good way for this is using Jupyter (notebook or lab) - which provides an interactive environment to analyze the data. ## Pro tips
The following helpers will help you loading the data into Pandas DataFrames, and may also give you some starting points in analyzing the results. * See [jupyter.org](https://jupyter.org/documentation) for usage instructions.
* Don't forget to start a Jupyter notebook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)*
* Copy the example notebook before use so your changes don't get clobbered with the next freqtrade update.
## Backtesting ## Fine print
To analyze your backtest results, you can [export the trades](#exporting-trades-to-file). Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually.
You can then load the trades to perform further analysis.
Freqtrade provides the `load_backtest_data()` helper function to easily load the backtest results, which takes the path to the the backtest-results file as parameter. ## Recommended workflow
``` python | Task | Tool |
--- | ---
Bot operations | CLI
Repetitive tasks | Shell scripts
Data analysis & visualization | Notebook
1. Use the CLI to
* download historical data
* run a backtest
* run with real-time data
* export results
1. Collect these actions in shell scripts
* save complicated commands with arguments
* execute multi-step operations
* automate testing strategies and preparing data for analysis
1. Use a notebook to
* visualize data
* munge and plot to generate insights
## Example utility snippets
### Change directory to root
Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistent.
```python
import os
from pathlib import Path
# Change directory
# Modify this cell to insure that the output shows the correct path.
# Define all paths relative to the project root shown in the cell output
project_root = "somedir/freqtrade"
i=0
try:
os.chdirdir(project_root)
assert Path('LICENSE').is_file()
except:
while i<4 and (not Path('LICENSE').is_file()):
os.chdir(Path(Path.cwd(), '../'))
i+=1
project_root = Path.cwd()
print(Path.cwd())
```
## Load existing objects into a Jupyter notebook
These examples assume that you have already generated data using the cli. They will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload.
### Load backtest results into a pandas dataframe
```python
from freqtrade.data.btanalysis import load_backtest_data from freqtrade.data.btanalysis import load_backtest_data
df = load_backtest_data("user_data/backtest-result.json")
# Load backtest results
df = load_backtest_data("user_data/backtest_results/backtest-result.json")
# Show value-counts per pair # Show value-counts per pair
df.groupby("pair")["sell_reason"].value_counts() df.groupby("pair")["sell_reason"].value_counts()
``` ```
This will allow you to drill deeper into your backtest results, and perform analysis which otherwise would make the regular backtest-output very difficult to digest due to information overload. ### Load live trading results into a pandas dataframe
If you have some ideas for interesting / helpful backtest data analysis ideas, please submit a Pull Request so the community can benefit from it.
## Live data
To analyze the trades your bot generated, you can load them to a DataFrame as follows:
``` python ``` python
from freqtrade.data.btanalysis import load_trades_from_db from freqtrade.data.btanalysis import load_trades_from_db
# Fetch trades from database
df = load_trades_from_db("sqlite:///tradesv3.sqlite") df = load_trades_from_db("sqlite:///tradesv3.sqlite")
# Display results
df.groupby("pair")["sell_reason"].value_counts() df.groupby("pair")["sell_reason"].value_counts()
```
### Load multiple configuration files
This option can be useful to inspect the results of passing in multiple configs
``` python
import json
from freqtrade.configuration import Configuration
# Load config from multiple files
config = Configuration.from_files(["config1.json", "config2.json"])
# Show the config in memory
print(json.dumps(config, indent=1))
```
### Load exchange data to a pandas dataframe
This loads candle data to a dataframe
```python
from pathlib import Path
from freqtrade.data.history import load_pair_history
# Load data using values passed to function
ticker_interval = "5m"
data_location = Path('user_data', 'data', 'bitrex')
pair = "BTC_USDT"
candles = load_pair_history(datadir=data_location,
ticker_interval=ticker_interval,
pair=pair)
# Confirm success
print(f"Loaded len(candles) rows of data for {pair} from {data_location}")
candles.head()
```
## Strategy debugging example
Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data.
### Define variables used in analyses
You can override strategy settings as demonstrated below.
```python
# Customize these according to your needs.
# Define some constants
ticker_interval = "5m"
# Name of the strategy class
strategy_name = 'TestStrategy'
# Path to user data
user_data_dir = 'user_data'
# Location of the strategy
strategy_location = Path(user_data_dir, 'strategies')
# Location of the data
data_location = Path(user_data_dir, 'data', 'binance')
# Pair to analyze - Only use one pair here
pair = "BTC_USDT"
```
### Load exchange data
```python
from pathlib import Path
from freqtrade.data.history import load_pair_history
# Load data using values set above
candles = load_pair_history(datadir=data_location,
ticker_interval=ticker_interval,
pair=pair)
# Confirm success
print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}")
candles.head()
```
### Load and run strategy
* Rerun each time the strategy file is changed
```python
from freqtrade.resolvers import StrategyResolver
# Load strategy using values set above
strategy = StrategyResolver({'strategy': strategy_name,
'user_data_dir': user_data_dir,
'strategy_path': strategy_location}).strategy
# Generate buy/sell signals using strategy
df = strategy.analyze_ticker(candles, {'pair': pair})
```
### Display the trade details
* Note that using `data.tail()` is preferable to `data.head()` as most indicators have some "startup" data at the top of the dataframe.
* Some possible problems
* Columns with NaN values at the end of the dataframe
* Columns used in `crossed*()` functions with completely different units
* Comparison with full backtest
* having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.
* Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available.
```python
# Report results
print(f"Generated {df['buy'].sum()} buy signals")
data = df.set_index('date', drop=True)
data.tail()
``` ```
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.

View File

@@ -4,28 +4,25 @@ This page contains description of the command line arguments, configuration para
and the bot features that were declared as DEPRECATED by the bot development team and the bot features that were declared as DEPRECATED by the bot development team
and are no longer supported. Please avoid their usage in your configuration. and are no longer supported. Please avoid their usage in your configuration.
## Deprecated
### the `--refresh-pairs-cached` command line option
`--refresh-pairs-cached` in the context of backtesting, hyperopt and edge allows to refresh candle data for backtesting.
Since this leads to much confusion, and slows down backtesting (while not being part of backtesting) this has been singled out
as a seperate freqtrade subcommand `freqtrade download-data`.
This command line option was deprecated in `2019.7-dev` and will be removed after the next release.
## Removed features
### The **--dynamic-whitelist** command line option ### The **--dynamic-whitelist** command line option
Per default `--dynamic-whitelist` will retrieve the 20 currencies based This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch)
on BaseVolume. This value can be changed when you run the script. and in freqtrade 2019.7 (master branch).
**By Default**
Get the 20 currencies based on BaseVolume.
```bash
python3 freqtrade --dynamic-whitelist
```
**Customize the number of currencies to retrieve**
Get the 30 currencies based on BaseVolume.
```bash
python3 freqtrade --dynamic-whitelist 30
```
**Exception**
`--dynamic-whitelist` must be greater than 0. If you enter 0 or a
negative value (e.g -2), `--dynamic-whitelist` will use the default
value (20).
### the `--live` command line option
`--live` in the context of backtesting allowed to download the latest tick data for backtesting.
Did only download the latest 500 candles, so was ineffective in getting good backtest data.
Removed in 2019-7-dev (develop branch) and in freqtrade 2019-8 (master branch)

View File

@@ -12,11 +12,34 @@ Special fields for the documentation (like Note boxes, ...) can be found [here](
## Developer setup ## Developer setup
To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ".
Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`. Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`.
This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`.
### Tests
New code should be covered by basic unittests. Depending on the complexity of the feature, Reviewers may request more in-depth unittests.
If necessary, the Freqtrade team can assist and give guidance with writing good tests (however please don't expect anyone to write the tests for you).
#### Checking log content in tests
Freqtrade uses 2 main methods to check log content in tests, `log_has()` and `log_has_re()` (to check using regex, in case of dynamic log-messages).
These are available from `conftest.py` and can be imported in any test module.
A sample check looks as follows:
``` python
from freqtrade.tests.conftest import log_has, log_has_re
def test_method_to_test(caplog):
method_to_test()
assert log_has("This event happened", caplog)
# Check regex with trailing number ...
assert log_has_re(r"This dynamic event happened and produced \d+", caplog)
```
## Modules ## Modules
### Dynamic Pairlist ### Dynamic Pairlist
@@ -130,7 +153,7 @@ If the day shows the same day, then the last candle can be assumed as incomplete
This part of the documentation is aimed at maintainers, and shows how to create a release. This part of the documentation is aimed at maintainers, and shows how to create a release.
### create release branch ### Create release branch
``` bash ``` bash
# make sure you're in develop branch # make sure you're in develop branch
@@ -140,11 +163,14 @@ git checkout develop
git checkout -b new_release git checkout -b new_release
``` ```
* Edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`) * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month.
* Commit this part * Commit this part
* push that branch to the remote and create a PR against the master branch * push that branch to the remote and create a PR against the master branch
### create changelog from git commits ### Create changelog from git commits
!!! Note
Make sure that both master and develop are up-todate!.
``` bash ``` bash
# Needs to be done before merging / pulling that branch. # Needs to be done before merging / pulling that branch.
@@ -153,6 +179,8 @@ git log --oneline --no-decorate --no-merges master..develop
### Create github release / tag ### Create github release / tag
Once the PR against master is merged (best right after merging):
* Use the button "Draft a new release" in the Github UI (subsection releases) * Use the button "Draft a new release" in the Github UI (subsection releases)
* Use the version-number specified as tag. * Use the version-number specified as tag.
* Use "master" as reference (this step comes after the above PR is merged). * Use "master" as reference (this step comes after the above PR is merged).
@@ -160,5 +188,5 @@ git log --oneline --no-decorate --no-merges master..develop
### After-release ### After-release
* Update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`). * Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`).
* Create a PR against develop to update that branch. * Create a PR against develop to update that branch.

View File

@@ -26,6 +26,10 @@ To update the image, simply run the above commands again and restart your runnin
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
!!! Note Docker image update frequency
The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate.
In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`.
### Prepare the configuration files ### Prepare the configuration files
Even though you will use docker, you'll still need some files from the github repository. Even though you will use docker, you'll still need some files from the github repository.
@@ -140,7 +144,7 @@ To run a restartable instance in the background (feel free to place your configu
#### Move your config file and database #### Move your config file and database
The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden folder in your home directory. Feel free to use a different folder and replace the folder in the upcomming commands. The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands.
```bash ```bash
mkdir ~/.freqtrade mkdir ~/.freqtrade

View File

@@ -3,7 +3,7 @@
This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss.
!!! Warning !!! Warning
Edge positioning is not compatible with dynamic whitelist. If enabled, it overrides the dynamic whitelist option. Edge positioning is not compatible with dynamic (volume-based) whitelist.
!!! Note !!! Note
Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation. Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation.
@@ -209,7 +209,7 @@ Edge will remove sudden pumps in a given market while going through historical d
You can run Edge independently in order to see in details the result. Here is an example: You can run Edge independently in order to see in details the result. Here is an example:
```bash ```bash
python3 freqtrade edge freqtrade edge
``` ```
An example of its output: An example of its output:
@@ -234,20 +234,19 @@ An example of its output:
### Update cached pairs with the latest data ### Update cached pairs with the latest data
```bash Edge requires historic data the same way as backtesting does.
python3 freqtrade edge --refresh-pairs-cached Please refer to the [download section](backtesting.md#Getting-data-for-backtesting-and-hyperopt) of the documentation for details.
```
### Precising stoploss range ### Precising stoploss range
```bash ```bash
python3 freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
``` ```
### Advanced use of timerange ### Advanced use of timerange
```bash ```bash
python3 freqtrade edge --timerange=20181110-20181113 freqtrade edge --timerange=20181110-20181113
``` ```
Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop. Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop.

View File

@@ -1,14 +1,25 @@
# Freqtrade FAQ # Freqtrade FAQ
### Freqtrade commons ## Freqtrade common issues
#### I have waited 5 minutes, why hasn't the bot made any trades yet?! ### The bot does not start
Running the bot with `freqtrade --config config.json` does show the output `freqtrade: command not found`.
This could have the following reasons:
* The virtual environment is not active
* run `source .env/bin/activate` to activate the virtual environment
* The installation did not work correctly.
* Please check the [Installation documentation](installation.md).
### I have waited 5 minutes, why hasn't the bot made any trades yet?!
Depending on the buy strategy, the amount of whitelisted coins, the Depending on the buy strategy, the amount of whitelisted coins, the
situation of the market etc, it can take up to hours to find good entry situation of the market etc, it can take up to hours to find good entry
position for a trade. Be patient! position for a trade. Be patient!
#### I have made 12 trades already, why is my total profit negative?! ### I have made 12 trades already, why is my total profit negative?!
I understand your disappointment but unfortunately 12 trades is just I understand your disappointment but unfortunately 12 trades is just
not enough to say anything. If you run backtesting, you can see that our not enough to say anything. If you run backtesting, you can see that our
@@ -19,24 +30,34 @@ of course constantly aim to improve the bot but it will _always_ be a
gamble, which should leave you with modest wins on monthly basis but gamble, which should leave you with modest wins on monthly basis but
you can't say much from few trades. you can't say much from few trades.
#### Id like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? ### Id like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again?
Not quite. Trades are persisted to a database but the configuration is Not quite. Trades are persisted to a database but the configuration is
currently only read when the bot is killed and restarted. `/stop` more currently only read when the bot is killed and restarted. `/stop` more
like pauses. You can stop your bot, adjust settings and start it again. like pauses. You can stop your bot, adjust settings and start it again.
#### I want to improve the bot with a new strategy ### I want to improve the bot with a new strategy
That's great. We have a nice backtesting and hyperoptimizing setup. See That's great. We have a nice backtesting and hyperoptimizing setup. See
the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
#### Is there a setting to only SELL the coins being held and not perform anymore BUYS? ### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
You can use the `/forcesell all` command from Telegram. You can use the `/forcesell all` command from Telegram.
### Hyperopt module ### I get the message "RESTRICTED_MARKET"
#### How many epoch do I need to get a good Hyperopt result? Currently known to happen for US Bittrex users.
Bittrex split its exchange into US and International versions.
The International version has more pairs available, however the API always returns all pairs, so there is currently no automated way to detect if you're affected by the restriction.
If you have restricted pairs in your whitelist, you'll get a warning message in the log on FreqTrade startup for each restricted pair.
If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you.
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist.
## Hyperopt module
### How many epoch do I need to get a good Hyperopt result?
Per default Hyperopts without `-e` or `--epochs` parameter will only Per default Hyperopts without `-e` or `--epochs` parameter will only
run 100 epochs, means 100 evals of your triggers, guards, ... Too few run 100 epochs, means 100 evals of your triggers, guards, ... Too few
@@ -47,16 +68,16 @@ compute.
We recommend you to run it at least 10.000 epochs: We recommend you to run it at least 10.000 epochs:
```bash ```bash
python3 freqtrade hyperopt -e 10000 freqtrade hyperopt -e 10000
``` ```
or if you want intermediate result to see or if you want intermediate result to see
```bash ```bash
for i in {1..100}; do python3 freqtrade hyperopt -e 100; done for i in {1..100}; do freqtrade hyperopt -e 100; done
``` ```
#### Why it is so long to run hyperopt? ### Why it is so long to run hyperopt?
Finding a great Hyperopt results takes time. Finding a great Hyperopt results takes time.
@@ -74,13 +95,14 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals.
Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th
of the search space. of the search space.
### Edge module ## Edge module
#### Edge implements interesting approach for controlling position size, is there any theory behind it? ### Edge implements interesting approach for controlling position size, is there any theory behind it?
The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members. The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members.
You can find further info on expectancy, winrate, risk management and position size in the following sources: You can find further info on expectancy, winrate, risk management and position size in the following sources:
- https://www.tradeciety.com/ultimate-math-guide-for-traders/ - https://www.tradeciety.com/ultimate-math-guide-for-traders/
- http://www.vantharp.com/tharp-concepts/expectancy.asp - http://www.vantharp.com/tharp-concepts/expectancy.asp
- https://samuraitradingacademy.com/trading-expectancy/ - https://samuraitradingacademy.com/trading-expectancy/

View File

@@ -18,23 +18,28 @@ Configuring hyperopt is similar to writing your own strategy, and many tasks wil
### Checklist on all tasks / possibilities in hyperopt ### Checklist on all tasks / possibilities in hyperopt
Depending on the space you want to optimize, only some of the below are required. Depending on the space you want to optimize, only some of the below are required:
* fill `populate_indicators` - probably a copy from your strategy * fill `populate_indicators` - probably a copy from your strategy
* fill `buy_strategy_generator` - for buy signal optimization * fill `buy_strategy_generator` - for buy signal optimization
* fill `indicator_space` - for buy signal optimzation * fill `indicator_space` - for buy signal optimzation
* fill `sell_strategy_generator` - for sell signal optimization * fill `sell_strategy_generator` - for sell signal optimization
* fill `sell_indicator_space` - for sell signal optimzation * fill `sell_indicator_space` - for sell signal optimzation
* fill `roi_space` - for ROI optimization
* fill `generate_roi_table` - for ROI optimization (if you need more than 3 entries) Optional, but recommended:
* fill `stoploss_space` - stoploss optimization
* Optional but recommended * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
Rarely you may also need to override:
* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default)
* `generate_roi_table` - for custom ROI optimization (if you need more than 4 entries in the ROI table)
* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default)
### 1. Install a Custom Hyperopt File ### 1. Install a Custom Hyperopt File
Put your hyperopt file into the folder`user_data/hyperopts`. Put your hyperopt file into the directory `user_data/hyperopts`.
Let assume you want a hyperopt file `awesome_hyperopt.py`: Let assume you want a hyperopt file `awesome_hyperopt.py`:
Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py` Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py`
@@ -144,21 +149,94 @@ it will end with telling you which paramter combination produced the best profit
The search for best parameters starts with a few random combinations and then uses a The search for best parameters starts with a few random combinations and then uses a
regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination
that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`. that minimizes the value of the [loss function](#loss-functions).
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
When you want to test an indicator that isn't used by the bot currently, remember to When you want to test an indicator that isn't used by the bot currently, remember to
add it to the `populate_indicators()` method in `hyperopt.py`. add it to the `populate_indicators()` method in `hyperopt.py`.
## Loss-functions
Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results.
By default, FreqTrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses.
A different loss function can be specified by using the `--hyperopt-loss <Class-name>` argument.
This class should be in its own file within the `user_data/hyperopts/` directory.
Currently, the following loss functions are builtin:
* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function)
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns)
### Creating and using a custom loss function
To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class.
For the sample below, you then need to add the command line parameter `--hyperopt-loss SuperDuperHyperOptLoss` to your hyperopt call so this fuction is being used.
A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_loss.py)
``` python
from freqtrade.optimize.hyperopt import IHyperOptLoss
TARGET_TRADES = 600
EXPECTED_MAX_PROFIT = 3.0
MAX_ACCEPTED_TRADE_DURATION = 300
class SuperDuperHyperOptLoss(IHyperOptLoss):
"""
Defines the default loss function for hyperopt
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results
This is the legacy algorithm (used until now in freqtrade).
Weights are distributed as follows:
* 0.4 to trade duration
* 0.25: Avoiding trade loss
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
"""
total_profit = results.profit_percent.sum()
trade_duration = results.trade_duration.mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
result = trade_loss + profit_loss + duration_loss
return result
```
Currently, the arguments are:
* `results`: DataFrame containing the result
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
`pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason`
* `trade_count`: Amount of trades (identical to `len(results)`)
* `min_date`: Start date of the hyperopting TimeFrame
* `min_date`: End date of the hyperopting TimeFrame
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.
!!! Note
This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily.
!!! Note
Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later.
## Execute Hyperopt ## Execute Hyperopt
Once you have updated your hyperopt configuration you can run it. Once you have updated your hyperopt configuration you can run it.
Because hyperopt tries a lot of combinations to find the best parameters it will take time you will have the result (more than 30 mins). Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results.
We strongly recommend to use `screen` or `tmux` to prevent any connection loss. We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
```bash ```bash
python3 freqtrade -c config.json hyperopt --customhyperopt <hyperoptname> -e 5000 --spaces all freqtrade -c config.json hyperopt --customhyperopt <hyperoptname> -e 5000 --spaces all
``` ```
Use `<hyperoptname>` as the name of the custom hyperopt used. Use `<hyperoptname>` as the name of the custom hyperopt used.
@@ -168,8 +246,11 @@ running at least several thousand evaluations.
The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below. The `--spaces all` flag determines that all possible parameters should be optimized. Possibilities are listed below.
!!! Note
By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`.
!!! Warning !!! Warning
When switching parameters or changing configuration options, the file `user_data/hyperopt_results.pickle` should be removed. It's used to be able to continue interrupted calculations, but does not detect changes to settings or the hyperopt file. When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed.
### Execute Hyperopt with Different Ticker-Data Source ### Execute Hyperopt with Different Ticker-Data Source
@@ -179,12 +260,11 @@ use data from directory `user_data/data`.
### Running Hyperopt with Smaller Testset ### Running Hyperopt with Smaller Testset
Use the `--timerange` argument to change how much of the testset Use the `--timerange` argument to change how much of the testset you want to use.
you want to use. The last N ticks/timeframes will be used. For example, to use one month of data, pass the following parameter to the hyperopt call:
Example:
```bash ```bash
python3 freqtrade hyperopt --timerange -200 freqtrade hyperopt --timerange 20180401-20180501
``` ```
### Running Hyperopt with Smaller Search Space ### Running Hyperopt with Smaller Search Space
@@ -197,12 +277,33 @@ new buy strategy you have.
Legal values are: Legal values are:
- `all`: optimize everything * `all`: optimize everything
- `buy`: just search for a new buy strategy * `buy`: just search for a new buy strategy
- `sell`: just search for a new sell strategy * `sell`: just search for a new sell strategy
- `roi`: just optimize the minimal profit table for your strategy * `roi`: just optimize the minimal profit table for your strategy
- `stoploss`: search for the best stoploss value * `stoploss`: search for the best stoploss value
- space-separated list of any of the above values for example `--spaces roi stoploss` * space-separated list of any of the above values for example `--spaces roi stoploss`
### Position stacking and disabling max market positions
In some situations, you may need to run Hyperopt (and Backtesting) with the
`--eps`/`--enable-position-staking` and `--dmmp`/`--disable-max-market-positions` arguments.
By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one
open trade is allowed for every traded pair. The total number of trades open for all pairs
is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to
some potential trades to be hidden (or masked) by previosly open trades.
The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times,
while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades`
during Hyperopt/Backtesting (which is equal to setting `max_open_trades` to a very high
number).
!!! Note
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
You can also enable position stacking in the configuration file by explicitly setting
`"position_stacking"=true`.
## Understand the Hyperopt Result ## Understand the Hyperopt Result
@@ -211,8 +312,10 @@ Given the following result from hyperopt:
``` ```
Best result: Best result:
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
with values: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
Buy hyperspace params:
{ 'adx-value': 44, { 'adx-value': 44,
'rsi-value': 29, 'rsi-value': 29,
'adx-enabled': False, 'adx-enabled': False,
@@ -231,7 +334,7 @@ method, what those values match to.
So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block:
``` ``` python
(dataframe['rsi'] < 29.0) (dataframe['rsi'] < 29.0)
``` ```
@@ -249,27 +352,25 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
return dataframe return dataframe
``` ```
By default, hyperopt prints colorized results -- epochs with positive profit are printed in the green color. This highlighting helps you find epochs that can be interesting for later analysis. Epochs with zero total profit or with negative profits (losses) are printed in the normal color. If you do not need colorization of results (for instance, when you are redirecting hyperopt output to a file) you can switch colorization off by specifying the `--no-color` option in the command line.
You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option.
### Understand Hyperopt ROI results ### Understand Hyperopt ROI results
If you are optimizing ROI, you're result will look as follows and include a ROI table. If you are optimizing ROI (i.e. if optimization search-space contains 'all' or 'roi'), your result will look as follows and include a ROI table:
``` ```
Best result: Best result:
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
with values: 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
Buy hyperspace params:
{ 'adx-value': 44, { 'adx-value': 44,
'rsi-value': 29, 'rsi-value': 29,
'adx-enabled': false, 'adx-enabled': False,
'rsi-enabled': True, 'rsi-enabled': True,
'trigger': 'bb_lower', 'trigger': 'bb_lower'}
'roi_t1': 40,
'roi_t2': 57,
'roi_t3': 21,
'roi_p1': 0.03634636907306948,
'roi_p2': 0.055237357937802885,
'roi_p3': 0.015163796015548354,
'stoploss': -0.37996664668703606
}
ROI table: ROI table:
{ 0: 0.10674752302642071, { 0: 0.10674752302642071,
21: 0.09158372701087236, 21: 0.09158372701087236,
@@ -280,27 +381,54 @@ ROI table:
This would translate to the following ROI table: This would translate to the following ROI table:
``` python ``` python
minimal_roi = { minimal_roi = {
"118": 0, "118": 0,
"78": 0.0363463, "78": 0.0363,
"21": 0.0915, "21": 0.0915,
"0": 0.106 "0": 0.106
} }
``` ```
### Validate backtest result If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps) with the values that can vary in the following ranges:
| # | minutes | ROI percentage |
|---|---|---|
| 1 | always 0 | 0.03...0.31 |
| 2 | 10...40 | 0.02...0.11 |
| 3 | 20...100 | 0.01...0.04 |
| 4 | 30...220 | always 0 |
This structure of the ROI table is sufficient in most cases. Override the `roi_space()` method defining the ranges desired if you need components of the ROI tables to vary in other ranges.
Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization in these methods if you need a different structure of the ROI table or other amount of rows (steps) in the ROI tables.
### Understand Hyperopt Stoploss results
If you are optimizing stoploss values (i.e. if optimization search-space contains 'all' or 'stoploss'), your result will look as follows and include stoploss:
```
Best result:
44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins. Objective: 1.94367
Buy hyperspace params:
{ 'adx-value': 44,
'rsi-value': 29,
'adx-enabled': False,
'rsi-enabled': True,
'trigger': 'bb_lower'}
Stoploss: -0.37996664668703606
```
If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.5...-0.02, which is sufficient in most cases.
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization.
### Validate backtesting results
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
To archive the same results (number of trades, ...) than during hyperopt, please use the command line flags `--disable-max-market-positions` and `--enable-position-stacking` for backtesting.
This configuration is the default in hyperopt for performance reasons. To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
You can overwrite position stacking in the configuration by explicitly setting `"position_stacking"=false` or by changing the relevant line in your hyperopt file [here](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L191).
Enabling the market-position for hyperopt is currently not possible.
!!! Note
Dry/live runs will **NOT** use position stacking - therefore it does make sense to also validate the strategy without this as it's closer to reality.
## Next Step ## Next Step

View File

@@ -4,12 +4,22 @@ This page explains how to prepare your environment for running the bot.
## Prerequisite ## Prerequisite
### Requirements
Click each one for install guide:
* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
* [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below)
### API keys
Before running your bot in production you will need to setup few Before running your bot in production you will need to setup few
external API. In production mode, the bot will require valid Exchange API external API. In production mode, the bot will require valid Exchange API
credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended).
- [Setup your exchange account](#setup-your-exchange-account)
### Setup your exchange account ### Setup your exchange account
You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script. You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script.
@@ -18,6 +28,9 @@ You will need to create API Keys (Usually you get `key` and `secret`) from the E
Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot.
!!! Note
Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case.
```bash ```bash
git clone git@github.com:freqtrade/freqtrade.git git clone git@github.com:freqtrade/freqtrade.git
cd freqtrade cd freqtrade
@@ -30,7 +43,7 @@ git checkout develop
## Easy Installation - Linux Script ## Easy Installation - Linux Script
If you are on Debian, Ubuntu or MacOS a freqtrade provides a script to Install, Update, Configure, and Reset your bot. If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot.
```bash ```bash
$ ./setup.sh $ ./setup.sh
@@ -45,7 +58,7 @@ usage:
This script will install everything you need to run the bot: This script will install everything you need to run the bot:
* Mandatory software as: `Python3`, `ta-lib`, `wget` * Mandatory software as: `ta-lib`
* Setup your virtualenv * Setup your virtualenv
* Configure your `config.json` file * Configure your `config.json` file
@@ -70,24 +83,16 @@ Config parameter is a `config.json` configurator. This script will ask you quest
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems.
### Requirements !!! Note
Python3.6 or higher and the corresponding pip are assumed to be available.
Click each one for install guide:
* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
* [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
### Linux - Ubuntu 16.04 ### Linux - Ubuntu 16.04
#### Install Python 3.6, Git, and wget #### Install necessary dependencies
```bash ```bash
sudo add-apt-repository ppa:jonathonf/python-3.6
sudo apt-get update sudo apt-get update
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git sudo apt-get install build-essential git
``` ```
#### Raspberry Pi / Raspbian #### Raspberry Pi / Raspbian
@@ -111,14 +116,6 @@ python3 -m pip install -r requirements-common.txt
python3 -m pip install -e . python3 -m pip install -e .
``` ```
### MacOS
#### Install Python 3.6, git and wget
```bash
brew install python3 git wget
```
### Common ### Common
#### 1. Install TA-Lib #### 1. Install TA-Lib
@@ -159,7 +156,7 @@ git clone https://github.com/freqtrade/freqtrade.git
``` ```
Optionally checkout the stable/master branch: Optionally checkout the master branch to get the latest stable release:
```bash ```bash
git checkout master git checkout master
@@ -177,9 +174,9 @@ cp config.json.example config.json
#### 5. Install python dependencies #### 5. Install python dependencies
``` bash ``` bash
pip3 install --upgrade pip python3 -m pip install --upgrade pip
pip3 install -r requirements.txt python3 -m pip install -r requirements.txt
pip3 install -e . python3 -m pip install -e .
``` ```
#### 6. Run the Bot #### 6. Run the Bot
@@ -187,7 +184,7 @@ pip3 install -e .
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
```bash ```bash
python3.6 freqtrade -c config.json freqtrade -c config.json
``` ```
*Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
@@ -217,11 +214,22 @@ when it changes.
The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd
as the watchdog. as the watchdog.
!!! Note !!! Note
The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container.
------ ------
## Using Conda
Freqtrade can also be installed using Anaconda (or Miniconda).
``` bash
conda env create -f environment.yml
```
!!! Note
This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first.
## Windows ## Windows
We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure).
@@ -237,8 +245,6 @@ If that is not available on your system, feel free to try the instructions below
git clone https://github.com/freqtrade/freqtrade.git git clone https://github.com/freqtrade/freqtrade.git
``` ```
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
#### Install ta-lib #### Install ta-lib
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).

View File

@@ -15,7 +15,7 @@ pip install -U -r requirements-plot.txt
Usage for the price plotter: Usage for the price plotter:
``` bash ``` bash
python3 script/plot_dataframe.py [-h] [-p pairs] [--live] python3 script/plot_dataframe.py [-h] [-p pairs]
``` ```
Example Example
@@ -41,20 +41,12 @@ To plot multiple pairs, separate them with a comma:
python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH
``` ```
To plot the current live price use the `--live` flag:
``` bash
python3 scripts/plot_dataframe.py -p BTC/ETH --live
```
To plot a timerange (to zoom in): To plot a timerange (to zoom in):
``` bash ``` bash
python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200 python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=20180801-20180805
``` ```
Timerange doesn't work with live data.
To plot trades stored in a database use `--db-url` argument: To plot trades stored in a database use `--db-url` argument:
``` bash ``` bash
@@ -64,7 +56,7 @@ python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p
To plot trades from a backtesting result, use `--export-filename <filename>` To plot trades from a backtesting result, use `--export-filename <filename>`
``` bash ``` bash
python3 scripts/plot_dataframe.py --export-filename user_data/backtest_data/backtest-result.json -p BTC/ETH python3 scripts/plot_dataframe.py --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH
``` ```
To plot a custom strategy the strategy should have first be backtested. To plot a custom strategy the strategy should have first be backtested.

View File

@@ -1 +1 @@
mkdocs-material==3.1.0 mkdocs-material==4.4.0

View File

@@ -16,10 +16,10 @@ Sample configuration:
}, },
``` ```
!!! Danger: Security warning !!! Danger Security warning
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
!!! Danger: Password selection !!! Danger Password selection
Please make sure to select a very strong, unique password to protect your bot from unauthorized access. Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly. You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly.
@@ -62,7 +62,7 @@ docker run -d \
``` ```
!!! Danger "Security warning" !!! Danger "Security warning"
By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot.
## Consuming the API ## Consuming the API

View File

@@ -5,8 +5,7 @@ indicators.
## Install a custom strategy file ## Install a custom strategy file
This is very simple. Copy paste your strategy file into the folder This is very simple. Copy paste your strategy file into the directory `user_data/strategies`.
`user_data/strategies`.
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`: Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`:
@@ -14,7 +13,7 @@ Let assume you have a class called `AwesomeStrategy` in the file `awesome-strate
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name) 2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
```bash ```bash
python3 freqtrade --strategy AwesomeStrategy freqtrade --strategy AwesomeStrategy
``` ```
## Change your strategy ## Change your strategy
@@ -22,7 +21,7 @@ python3 freqtrade --strategy AwesomeStrategy
The bot includes a default strategy file. However, we recommend you to The bot includes a default strategy file. However, we recommend you to
use your own file to not have to lose your parameters every time the default use your own file to not have to lose your parameters every time the default
strategy file will be updated on Github. Put your custom strategy file strategy file will be updated on Github. Put your custom strategy file
into the folder `user_data/strategies`. into the directory `user_data/strategies`.
Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes.
`cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py` `cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py`
@@ -41,7 +40,7 @@ The bot also include a sample strategy called `TestStrategy` you can update: `us
You can test it with the parameter: `--strategy TestStrategy` You can test it with the parameter: `--strategy TestStrategy`
```bash ```bash
python3 freqtrade --strategy AwesomeStrategy freqtrade --strategy AwesomeStrategy
``` ```
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py) **For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
@@ -275,27 +274,24 @@ Please always check the mode of operation to select the correct method to get da
#### Possible options for DataProvider #### Possible options for DataProvider
- `available_pairs` - Property with tuples listing cached pairs with their intervals. (pair, interval) - `available_pairs` - Property with tuples listing cached pairs with their intervals (pair, interval).
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for all pairs in the whitelist, returns DataFrame or empty DataFrame - `ohlcv(pair, ticker_interval)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame.
- `historic_ohlcv(pair, ticker_interval)` - Data stored on disk - `historic_ohlcv(pair, ticker_interval)` - Returns historical data stored on disk.
- `get_pair_dataframe(pair, ticker_interval)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
- `runmode` - Property containing the current runmode. - `runmode` - Property containing the current runmode.
#### ohlcv / historic_ohlcv #### Example: fetch live ohlcv / historic data for the first informative pair
``` python ``` python
if self.dp: if self.dp:
if self.dp.runmode in ('live', 'dry_run'): inf_pair, inf_timeframe = self.informative_pairs()[0]
if (f'{self.stake_currency}/BTC', self.ticker_interval) in self.dp.available_pairs: informative = self.dp.get_pair_dataframe(pair=inf_pair,
data_eth = self.dp.ohlcv(pair='{self.stake_currency}/BTC', ticker_interval=inf_timeframe)
ticker_interval=self.ticker_interval)
else:
# Get historic ohlcv data (cached on disk).
history_eth = self.dp.historic_ohlcv(pair='{self.stake_currency}/BTC',
ticker_interval='1h')
``` ```
!!! Warning Warning about backtesting !!! Warning Warning about backtesting
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` provides the full time-range in one go, Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
for the backtesting runmode) provides the full time-range in one go,
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode). so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
!!! Warning Warning in hyperopt !!! Warning Warning in hyperopt
@@ -310,8 +306,10 @@ if self.dp:
dataframe['best_bid'] = ob['bids'][0][0] dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0] dataframe['best_ask'] = ob['asks'][0][0]
``` ```
!Warning The order book is not part of the historic data which means backtesting and hyperopt will not work if this
method is used. !!! Warning
The order book is not part of the historic data which means backtesting and hyperopt will not work if this
method is used.
#### Available Pairs #### Available Pairs
@@ -321,7 +319,6 @@ if self.dp:
print(f"available {pair}, {ticker}") print(f"available {pair}, {ticker}")
``` ```
#### Get data for non-tradeable pairs #### Get data for non-tradeable pairs
Data for additional, informative pairs (reference pairs) can be beneficial for some strategies. Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
@@ -398,10 +395,10 @@ The default buy strategy is located in the file
### Specify custom strategy location ### Specify custom strategy location
If you want to use a strategy from a different folder you can pass `--strategy-path` If you want to use a strategy from a different directory you can pass `--strategy-path`
```bash ```bash
python3 freqtrade --strategy AwesomeStrategy --strategy-path /some/folder freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
### Further strategy ideas ### Further strategy ideas

59
environment.yml Normal file
View File

@@ -0,0 +1,59 @@
name: freqtrade
channels:
- defaults
- conda-forge
dependencies:
# Required for app
- python>=3.6
- pip
- wheel
- numpy
- pandas
- scipy
- SQLAlchemy
- scikit-learn
- arrow
- requests
- urllib3
- wrapt
- joblib
- jsonschema
- tabulate
- python-rapidjson
- filelock
- flask
- python-dotenv
- cachetools
- scikit-optimize
- python-telegram-bot
# Optional for plotting
- plotly
# Optional for development
- flake8
- pytest
- pytest-mock
- pytest-asyncio
- pytest-cov
- coveralls
- mypy
# Useful for jupyter
- jupyter
- ipykernel
- isort
- yapf
- pip:
# Required for app
- cython
- coinmarketcap
- ccxt
- TA-Lib
- py_find_1st
- sdnotify
# Optional for develpment
- flake8-tidy-imports
- flake8-type-annotations
- pytest-random-order
- -e .

View File

@@ -1,5 +1,5 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '2019.6' __version__ = '2019.8-1'
class DependencyException(Exception): class DependencyException(Exception):

View File

@@ -1,526 +0,0 @@
"""
This module contains the argument manager class
"""
import argparse
import os
import re
from typing import List, NamedTuple, Optional
import arrow
from freqtrade import __version__, constants
class TimeRange(NamedTuple):
"""
NamedTuple Defining timerange inputs.
[start/stop]type defines if [start/stop]ts shall be used.
if *type is none, don't use corresponding startvalue.
"""
starttype: Optional[str] = None
stoptype: Optional[str] = None
startts: int = 0
stopts: int = 0
class Arguments(object):
"""
Arguments Class. Manage the arguments received by the cli
"""
def __init__(self, args: Optional[List[str]], description: str) -> None:
self.args = args
self.parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description=description)
def _load_args(self) -> None:
self.common_options()
self.main_options()
self._build_subcommands()
def get_parsed_arg(self) -> argparse.Namespace:
"""
Return the list of arguments
:return: List[str] List of arguments
"""
if self.parsed_arg is None:
self._load_args()
self.parsed_arg = self.parse_args()
return self.parsed_arg
def parse_args(self, no_default_config: bool = False) -> argparse.Namespace:
"""
Parses given arguments and returns an argparse Namespace instance.
"""
parsed_arg = self.parser.parse_args(self.args)
# Workaround issue in argparse with action='append' and default value
# (see https://bugs.python.org/issue16399)
if not no_default_config and parsed_arg.config is None:
parsed_arg.config = [constants.DEFAULT_CONFIG]
return parsed_arg
def common_options(self) -> None:
"""
Parses arguments that are common for the main Freqtrade, all subcommands and scripts.
"""
parser = self.parser
parser.add_argument(
'-v', '--verbose',
help='Verbose mode (-vv for more, -vvv to get all messages).',
action='count',
dest='loglevel',
default=0,
)
parser.add_argument(
'--logfile',
help='Log to the file specified.',
dest='logfile',
metavar='FILE',
)
parser.add_argument(
'--version',
action='version',
version=f'%(prog)s {__version__}'
)
parser.add_argument(
'-c', '--config',
help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). '
f'Multiple --config options may be used. '
f'Can be set to `-` to read config from stdin.',
dest='config',
action='append',
metavar='PATH',
)
parser.add_argument(
'-d', '--datadir',
help='Path to backtest data.',
dest='datadir',
metavar='PATH',
)
def main_options(self) -> None:
"""
Parses arguments for the main Freqtrade.
"""
parser = self.parser
parser.add_argument(
'-s', '--strategy',
help='Specify strategy class name (default: `%(default)s`).',
dest='strategy',
default='DefaultStrategy',
metavar='NAME',
)
parser.add_argument(
'--strategy-path',
help='Specify additional strategy lookup path.',
dest='strategy_path',
metavar='PATH',
)
parser.add_argument(
'--dynamic-whitelist',
help='Dynamically generate and update whitelist '
'based on 24h BaseVolume (default: %(const)s). '
'DEPRECATED.',
dest='dynamic_whitelist',
const=constants.DYNAMIC_WHITELIST,
type=int,
metavar='INT',
nargs='?',
)
parser.add_argument(
'--db-url',
help=f'Override trades database URL, this is useful in custom deployments '
f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, '
f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).',
dest='db_url',
metavar='PATH',
)
parser.add_argument(
'--sd-notify',
help='Notify systemd service manager.',
action='store_true',
dest='sd_notify',
)
def common_optimize_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses arguments common for Backtesting, Edge and Hyperopt modules.
:param parser:
"""
parser = subparser or self.parser
parser.add_argument(
'-i', '--ticker-interval',
help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).',
dest='ticker_interval',
)
parser.add_argument(
'--timerange',
help='Specify what timerange of data to use.',
dest='timerange',
)
parser.add_argument(
'--max_open_trades',
help='Specify max_open_trades to use.',
type=int,
dest='max_open_trades',
)
parser.add_argument(
'--stake_amount',
help='Specify stake_amount.',
type=float,
dest='stake_amount',
)
parser.add_argument(
'-r', '--refresh-pairs-cached',
help='Refresh the pairs files in tests/testdata with the latest data from the '
'exchange. Use it if you want to run your optimization commands with '
'up-to-date data.',
action='store_true',
dest='refresh_pairs',
)
def backtesting_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses given arguments for Backtesting module.
"""
parser = subparser or self.parser
parser.add_argument(
'--eps', '--enable-position-stacking',
help='Allow buying the same pair multiple times (position stacking).',
action='store_true',
dest='position_stacking',
default=False
)
parser.add_argument(
'--dmmp', '--disable-max-market-positions',
help='Disable applying `max_open_trades` during backtest '
'(same as setting `max_open_trades` to a very high number).',
action='store_false',
dest='use_max_market_positions',
default=True
)
parser.add_argument(
'-l', '--live',
help='Use live data.',
action='store_true',
dest='live',
)
parser.add_argument(
'--strategy-list',
help='Provide a comma-separated list of strategies to backtest. '
'Please note that ticker-interval needs to be set either in config '
'or via command line. When using this together with `--export trades`, '
'the strategy-name is injected into the filename '
'(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`',
nargs='+',
dest='strategy_list',
)
parser.add_argument(
'--export',
help='Export backtest results, argument are: trades. '
'Example: `--export=trades`',
dest='export',
)
parser.add_argument(
'--export-filename',
help='Save backtest results to the file with this filename (default: `%(default)s`). '
'Requires `--export` to be set as well. '
'Example: `--export-filename=user_data/backtest_data/backtest_today.json`',
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
dest='exportfilename',
metavar='PATH',
)
def edge_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses given arguments for Edge module.
"""
parser = subparser or self.parser
parser.add_argument(
'--stoplosses',
help='Defines a range of stoploss values against which edge will assess the strategy. '
'The format is "min,max,step" (without any space). '
'Example: `--stoplosses=-0.01,-0.1,-0.001`',
dest='stoploss_range',
)
def hyperopt_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses given arguments for Hyperopt module.
"""
parser = subparser or self.parser
parser.add_argument(
'--customhyperopt',
help='Specify hyperopt class name (default: `%(default)s`).',
dest='hyperopt',
default=constants.DEFAULT_HYPEROPT,
metavar='NAME',
)
parser.add_argument(
'--eps', '--enable-position-stacking',
help='Allow buying the same pair multiple times (position stacking).',
action='store_true',
dest='position_stacking',
default=False
)
parser.add_argument(
'--dmmp', '--disable-max-market-positions',
help='Disable applying `max_open_trades` during backtest '
'(same as setting `max_open_trades` to a very high number).',
action='store_false',
dest='use_max_market_positions',
default=True
)
parser.add_argument(
'-e', '--epochs',
help='Specify number of epochs (default: %(default)d).',
dest='epochs',
default=constants.HYPEROPT_EPOCH,
type=int,
metavar='INT',
)
parser.add_argument(
'-s', '--spaces',
help='Specify which parameters to hyperopt. Space-separated list. '
'Default: `%(default)s`.',
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
default='all',
nargs='+',
dest='spaces',
)
parser.add_argument(
'--print-all',
help='Print all results, not only the best ones.',
action='store_true',
dest='print_all',
default=False
)
parser.add_argument(
'-j', '--job-workers',
help='The number of concurrently running jobs for hyperoptimization '
'(hyperopt worker processes). '
'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. '
'If 1 is given, no parallel computing code is used at all.',
dest='hyperopt_jobs',
default=-1,
type=int,
metavar='JOBS',
)
parser.add_argument(
'--random-state',
help='Set random state to some positive integer for reproducible hyperopt results.',
dest='hyperopt_random_state',
type=Arguments.check_int_positive,
metavar='INT',
)
parser.add_argument(
'--min-trades',
help="Set minimal desired number of trades for evaluations in the hyperopt "
"optimization path (default: 1).",
dest='hyperopt_min_trades',
default=1,
type=Arguments.check_int_positive,
metavar='INT',
)
def list_exchanges_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses given arguments for the list-exchanges command.
"""
parser = subparser or self.parser
parser.add_argument(
'-1', '--one-column',
help='Print exchanges in one column.',
action='store_true',
dest='print_one_column',
)
def _build_subcommands(self) -> None:
"""
Builds and attaches all subcommands.
:return: None
"""
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
from freqtrade.utils import start_list_exchanges
subparsers = self.parser.add_subparsers(dest='subparser')
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.')
backtesting_cmd.set_defaults(func=start_backtesting)
self.common_optimize_options(backtesting_cmd)
self.backtesting_options(backtesting_cmd)
# Add edge subcommand
edge_cmd = subparsers.add_parser('edge', help='Edge module.')
edge_cmd.set_defaults(func=start_edge)
self.common_optimize_options(edge_cmd)
self.edge_options(edge_cmd)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.')
hyperopt_cmd.set_defaults(func=start_hyperopt)
self.common_optimize_options(hyperopt_cmd)
self.hyperopt_options(hyperopt_cmd)
# Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser(
'list-exchanges',
help='Print available exchanges.'
)
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
self.list_exchanges_options(list_exchanges_cmd)
@staticmethod
def parse_timerange(text: Optional[str]) -> TimeRange:
"""
Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange
:return: Start and End range period
"""
if text is None:
return TimeRange(None, None, 0, 0)
syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
for rex, stype in syntax:
# Apply the regular expression to text
match = re.match(rex, text)
if match: # Regex has matched
rvals = match.groups()
index = 0
start: int = 0
stop: int = 0
if stype[0]:
starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8:
start = arrow.get(starts, 'YYYYMMDD').timestamp
else:
start = int(starts)
index += 1
if stype[1]:
stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8:
stop = arrow.get(stops, 'YYYYMMDD').timestamp
else:
stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop)
raise Exception('Incorrect syntax for timerange "%s"' % text)
@staticmethod
def check_int_positive(value: str) -> int:
try:
uint = int(value)
if uint <= 0:
raise ValueError
except ValueError:
raise argparse.ArgumentTypeError(
f"{value} is invalid for this parameter, should be a positive integer value"
)
return uint
def common_scripts_options(self, subparser: argparse.ArgumentParser = None) -> None:
"""
Parses arguments common for scripts.
"""
parser = subparser or self.parser
parser.add_argument(
'-p', '--pairs',
help='Show profits for only these pairs. Pairs are comma-separated.',
dest='pairs',
)
def download_data_options(self) -> None:
"""
Parses given arguments for testdata download script
"""
parser = self.parser
parser.add_argument(
'--pairs-file',
help='File containing a list of pairs to download.',
dest='pairs_file',
metavar='FILE',
)
parser.add_argument(
'--days',
help='Download data for given number of days.',
dest='days',
type=Arguments.check_int_positive,
metavar='INT',
)
parser.add_argument(
'--exchange',
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
f'Only valid if no config is provided.',
dest='exchange',
)
parser.add_argument(
'-t', '--timeframes',
help=f'Specify which tickers to download. Space-separated list. '
f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.',
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
'6h', '8h', '12h', '1d', '3d', '1w'],
nargs='+',
dest='timeframes',
)
parser.add_argument(
'--erase',
help='Clean all existing data for the selected exchange/pairs/timeframes.',
dest='erase',
action='store_true'
)
def plot_dataframe_options(self) -> None:
"""
Parses given arguments for plot dataframe script
"""
parser = self.parser
parser.add_argument(
'--indicators1',
help='Set indicators from your strategy you want in the first row of the graph. '
'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.',
default='sma,ema3,ema5',
dest='indicators1',
)
parser.add_argument(
'--indicators2',
help='Set indicators from your strategy you want in the third row of the graph. '
'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.',
default='macd,macdsignal',
dest='indicators2',
)
parser.add_argument(
'--plot-limit',
help='Specify tick limit for plotting. Notice: too high values cause huge files. '
'Default: %(default)s.',
dest='plot_limit',
default=750,
type=int,
)
parser.add_argument(
'--trade-source',
help='Specify the source for trades (Can be DB or file (backtest file)) '
'Default: %(default)s',
dest='trade_source',
default="file",
choices=["DB", "file"]
)

View File

@@ -1,458 +0,0 @@
"""
This module contains the configuration class
"""
import json
import logging
import os
import sys
from argparse import Namespace
from logging.handlers import RotatingFileHandler
from typing import Any, Callable, Dict, List, Optional
from jsonschema import Draft4Validator, validators
from jsonschema.exceptions import ValidationError, best_match
from freqtrade import OperationalException, constants
from freqtrade.exchange import (is_exchange_bad, is_exchange_available,
is_exchange_officially_supported, available_exchanges)
from freqtrade.misc import deep_merge_dicts
from freqtrade.state import RunMode
logger = logging.getLogger(__name__)
def set_loggers(log_level: int = 0) -> None:
"""
Set the logger level for Third party libs
:return: None
"""
logging.getLogger('requests').setLevel(logging.INFO if log_level <= 1 else logging.DEBUG)
logging.getLogger("urllib3").setLevel(logging.INFO if log_level <= 1 else logging.DEBUG)
logging.getLogger('ccxt.base.exchange').setLevel(
logging.INFO if log_level <= 2 else logging.DEBUG)
logging.getLogger('telegram').setLevel(logging.INFO)
def _extend_validator(validator_class):
"""
Extended validator for the Freqtrade configuration JSON Schema.
Currently it only handles defaults for subschemas.
"""
validate_properties = validator_class.VALIDATORS['properties']
def set_defaults(validator, properties, instance, schema):
for prop, subschema in properties.items():
if 'default' in subschema:
instance.setdefault(prop, subschema['default'])
for error in validate_properties(
validator, properties, instance, schema,
):
yield error
return validators.extend(
validator_class, {'properties': set_defaults}
)
FreqtradeValidator = _extend_validator(Draft4Validator)
class Configuration(object):
"""
Class to read and init the bot configuration
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
"""
def __init__(self, args: Namespace, runmode: RunMode = None) -> None:
self.args = args
self.config: Optional[Dict[str, Any]] = None
self.runmode = runmode
def load_config(self) -> Dict[str, Any]:
"""
Extract information for sys.argv and load the bot configuration
:return: Configuration dictionary
"""
config: Dict[str, Any] = {}
# Now expecting a list of config filenames here, not a string
for path in self.args.config:
logger.info('Using config: %s ...', path)
# Merge config options, overwriting old values
config = deep_merge_dicts(self._load_config_file(path), config)
if 'internals' not in config:
config['internals'] = {}
logger.info('Validating configuration ...')
self._validate_config_schema(config)
self._validate_config_consistency(config)
# Set strategy if not specified in config and or if it's non default
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'):
config.update({'strategy': self.args.strategy})
if self.args.strategy_path:
config.update({'strategy_path': self.args.strategy_path})
# Load Common configuration
config = self._load_common_config(config)
# Load Optimize configurations
config = self._load_optimize_config(config)
# Add plotting options if available
config = self._load_plot_config(config)
# Set runmode
if not self.runmode:
# Handle real mode, infer dry/live from config
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
config.update({'runmode': self.runmode})
return config
def _load_config_file(self, path: str) -> Dict[str, Any]:
"""
Loads a config file from the given path
:param path: path as str
:return: configuration as dictionary
"""
try:
# Read config from stdin if requested in the options
with open(path) if path != '-' else sys.stdin as file:
conf = json.load(file)
except FileNotFoundError:
raise OperationalException(
f'Config file "{path}" not found!'
' Please create a config file or check whether it exists.')
return conf
def _load_logging_config(self, config: Dict[str, Any]) -> None:
"""
Extract information for sys.argv and load logging configuration:
the --loglevel, --logfile options
"""
# Log level
if 'loglevel' in self.args and self.args.loglevel:
config.update({'verbosity': self.args.loglevel})
else:
config.update({'verbosity': 0})
# Log to stdout, not stderr
log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)]
if 'logfile' in self.args and self.args.logfile:
config.update({'logfile': self.args.logfile})
# Allow setting this as either configuration or argument
if 'logfile' in config:
log_handlers.append(RotatingFileHandler(config['logfile'],
maxBytes=1024 * 1024, # 1Mb
backupCount=10))
logging.basicConfig(
level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=log_handlers
)
set_loggers(config['verbosity'])
logger.info('Verbosity set to %s', config['verbosity'])
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load common configuration
:return: configuration as dictionary
"""
self._load_logging_config(config)
# Support for sd_notify
if self.args.sd_notify:
config['internals'].update({'sd_notify': True})
# Add dynamic_whitelist if found
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
# Update to volumePairList (the previous default)
config['pairlist'] = {'method': 'VolumePairList',
'config': {'number_assets': self.args.dynamic_whitelist}
}
logger.warning(
'Parameter --dynamic-whitelist has been deprecated, '
'and will be completely replaced by the whitelist dict in the future. '
'For now: using dynamically generated whitelist based on VolumePairList. '
'(not applicable with Backtesting and Hyperopt)'
)
if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL:
config.update({'db_url': self.args.db_url})
logger.info('Parameter --db-url detected ...')
if config.get('dry_run', False):
logger.info('Dry run is enabled')
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
# Default to in-memory db for dry_run if not specified
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
else:
if not config.get('db_url', None):
config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled')
if config.get('forcebuy_enable', False):
logger.warning('`forcebuy` RPC message enabled.')
# Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf')
logger.info(f'Using DB: "{config["db_url"]}"')
# Check if the exchange set by the user is supported
self.check_exchange(config)
return config
def _create_datadir(self, config: Dict[str, Any], datadir: Optional[str] = None) -> str:
if not datadir:
# set datadir
exchange_name = config.get('exchange', {}).get('name').lower()
datadir = os.path.join('user_data', 'data', exchange_name)
if not os.path.isdir(datadir):
os.makedirs(datadir)
logger.info(f'Created data directory: {datadir}')
return datadir
def _args_to_config(self, config: Dict[str, Any], argname: str,
logstring: str, logfun: Optional[Callable] = None) -> None:
"""
:param config: Configuration dictionary
:param argname: Argumentname in self.args - will be copied to config dict.
:param logstring: Logging String
:param logfun: logfun is applied to the configuration entry before passing
that entry to the log string using .format().
sample: logfun=len (prints the length of the found
configuration instead of the content)
"""
if argname in self.args and getattr(self.args, argname):
config.update({argname: getattr(self.args, argname)})
if logfun:
logger.info(logstring.format(logfun(config[argname])))
else:
logger.info(logstring.format(config[argname]))
def _load_datadir_config(self, config: Dict[str, Any]) -> None:
"""
Extract information for sys.argv and load datadir configuration:
the --datadir option
"""
if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': self._create_datadir(config, self.args.datadir)})
else:
config.update({'datadir': self._create_datadir(config, None)})
logger.info('Using data folder: %s ...', config.get('datadir'))
def _load_optimize_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load Optimize configuration
:return: configuration as dictionary
"""
# This will override the strategy configuration
self._args_to_config(config, argname='ticker_interval',
logstring='Parameter -i/--ticker-interval detected ... '
'Using ticker_interval: {} ...')
self._args_to_config(config, argname='live',
logstring='Parameter -l/--live detected ...')
self._args_to_config(config, argname='position_stacking',
logstring='Parameter --enable-position-stacking detected ...')
if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions:
config.update({'use_max_market_positions': False})
logger.info('Parameter --disable-max-market-positions detected ...')
logger.info('max_open_trades set to unlimited ...')
elif 'max_open_trades' in self.args and self.args.max_open_trades:
config.update({'max_open_trades': self.args.max_open_trades})
logger.info('Parameter --max_open_trades detected, '
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
else:
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
self._args_to_config(config, argname='stake_amount',
logstring='Parameter --stake_amount detected, '
'overriding stake_amount to: {} ...')
self._args_to_config(config, argname='timerange',
logstring='Parameter --timerange detected: {} ...')
self._load_datadir_config(config)
self._args_to_config(config, argname='refresh_pairs',
logstring='Parameter -r/--refresh-pairs-cached detected ...')
self._args_to_config(config, argname='strategy_list',
logstring='Using strategy list of {} Strategies', logfun=len)
self._args_to_config(config, argname='ticker_interval',
logstring='Overriding ticker interval with Command line argument')
self._args_to_config(config, argname='export',
logstring='Parameter --export detected: {} ...')
self._args_to_config(config, argname='exportfilename',
logstring='Storing backtest results to {} ...')
# Edge section:
if 'stoploss_range' in self.args and self.args.stoploss_range:
txt_range = eval(self.args.stoploss_range)
config['edge'].update({'stoploss_range_min': txt_range[0]})
config['edge'].update({'stoploss_range_max': txt_range[1]})
config['edge'].update({'stoploss_range_step': txt_range[2]})
logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range)
# Hyperopt section
self._args_to_config(config, argname='hyperopt',
logstring='Using Hyperopt file {}')
self._args_to_config(config, argname='epochs',
logstring='Parameter --epochs detected ... '
'Will run Hyperopt with for {} epochs ...'
)
self._args_to_config(config, argname='spaces',
logstring='Parameter -s/--spaces detected: {}')
self._args_to_config(config, argname='print_all',
logstring='Parameter --print-all detected ...')
self._args_to_config(config, argname='hyperopt_jobs',
logstring='Parameter -j/--job-workers detected: {}')
self._args_to_config(config, argname='hyperopt_random_state',
logstring='Parameter --random-state detected: {}')
self._args_to_config(config, argname='hyperopt_min_trades',
logstring='Parameter --min-trades detected: {}')
return config
def _load_plot_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv Plotting configuration
:return: configuration as dictionary
"""
self._args_to_config(config, argname='pairs',
logstring='Using pairs {}')
self._args_to_config(config, argname='indicators1',
logstring='Using indicators1: {}')
self._args_to_config(config, argname='indicators2',
logstring='Using indicators2: {}')
self._args_to_config(config, argname='plot_limit',
logstring='Limiting plot to: {}')
self._args_to_config(config, argname='trade_source',
logstring='Using trades from: {}')
return config
def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate the configuration follow the Config Schema
:param conf: Config in JSON format
:return: Returns the config if valid, otherwise throw an exception
"""
try:
FreqtradeValidator(constants.CONF_SCHEMA).validate(conf)
return conf
except ValidationError as exception:
logger.critical(
'Invalid configuration. See config.json.example. Reason: %s',
exception
)
raise ValidationError(
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
)
def _validate_config_consistency(self, conf: Dict[str, Any]) -> None:
"""
Validate the configuration consistency
:param conf: Config in JSON format
:return: Returns None if everything is ok, otherwise throw an OperationalException
"""
# validating trailing stoploss
self._validate_trailing_stoploss(conf)
def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None:
# Skip if trailing stoploss is not activated
if not conf.get('trailing_stop', False):
return
tsl_positive = float(conf.get('trailing_stop_positive', 0))
tsl_offset = float(conf.get('trailing_stop_positive_offset', 0))
tsl_only_offset = conf.get('trailing_only_offset_is_reached', False)
if tsl_only_offset:
if tsl_positive == 0.0:
raise OperationalException(
f'The config trailing_only_offset_is_reached needs '
'trailing_stop_positive_offset to be more than 0 in your config.')
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
raise OperationalException(
f'The config trailing_stop_positive_offset needs '
'to be greater than trailing_stop_positive_offset in your config.')
def get_config(self) -> Dict[str, Any]:
"""
Return the config. Use this method to get the bot config
:return: Dict: Bot config
"""
if self.config is None:
self.config = self.load_config()
return self.config
def check_exchange(self, config: Dict[str, Any], check_for_bad: bool = True) -> bool:
"""
Check if the exchange name in the config file is supported by Freqtrade
:param check_for_bad: if True, check the exchange against the list of known 'bad'
exchanges
:return: False if exchange is 'bad', i.e. is known to work with the bot with
critical issues or does not work at all, crashes, etc. True otherwise.
raises an exception if the exchange if not supported by ccxt
and thus is not known for the Freqtrade at all.
"""
logger.info("Checking exchange...")
exchange = config.get('exchange', {}).get('name').lower()
if not is_exchange_available(exchange):
raise OperationalException(
f'Exchange "{exchange}" is not supported by ccxt '
f'and therefore not available for the bot.\n'
f'The following exchanges are supported by ccxt: '
f'{", ".join(available_exchanges())}'
)
if check_for_bad and is_exchange_bad(exchange):
logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. '
f'Use it only for development and testing purposes.')
return False
if is_exchange_officially_supported(exchange):
logger.info(f'Exchange "{exchange}" is officially supported '
f'by the Freqtrade development team.')
else:
logger.warning(f'Exchange "{exchange}" is supported by ccxt '
f'and therefore available for the bot but not officially supported '
f'by the Freqtrade development team. '
f'It may work flawlessly (please report back) or have serious issues. '
f'Use it at your own discretion.')
return True

View File

@@ -0,0 +1,4 @@
from freqtrade.configuration.arguments import Arguments # noqa: F401
from freqtrade.configuration.timerange import TimeRange # noqa: F401
from freqtrade.configuration.configuration import Configuration # noqa: F401
from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401

View File

@@ -0,0 +1,141 @@
"""
This module contains the argument manager class
"""
import argparse
from typing import List, Optional
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
from freqtrade import constants
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
ARGS_STRATEGY = ["strategy", "strategy_path"]
ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"]
ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange",
"max_open_trades", "stake_amount", "refresh_pairs"]
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
"strategy_list", "export", "exportfilename"]
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"position_stacking", "epochs", "spaces",
"use_max_market_positions", "print_all",
"print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades",
"hyperopt_continue", "hyperopt_loss"]
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
ARGS_LIST_EXCHANGES = ["print_one_column"]
ARGS_CREATE_USERDIR = ["user_data_dir"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"]
ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY +
["pairs", "indicators1", "indicators2", "plot_limit", "db_url",
"trade_source", "export", "exportfilename", "timerange",
"refresh_pairs"])
ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY +
["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"])
NO_CONF_REQURIED = ["start_download_data"]
class Arguments(object):
"""
Arguments Class. Manage the arguments received by the cli
"""
def __init__(self, args: Optional[List[str]], description: str,
no_default_config: bool = False) -> None:
self.args = args
self._parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description=description)
self._no_default_config = no_default_config
def _load_args(self) -> None:
self._build_args(optionlist=ARGS_MAIN)
self._build_subcommands()
def get_parsed_arg(self) -> argparse.Namespace:
"""
Return the list of arguments
:return: List[str] List of arguments
"""
if self._parsed_arg is None:
self._load_args()
self._parsed_arg = self._parse_args()
return self._parsed_arg
def _parse_args(self) -> argparse.Namespace:
"""
Parses given arguments and returns an argparse Namespace instance.
"""
parsed_arg = self.parser.parse_args(self.args)
# Workaround issue in argparse with action='append' and default value
# (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting)
if (not self._no_default_config and parsed_arg.config is None
and not (hasattr(parsed_arg, 'func')
and parsed_arg.func.__name__ in NO_CONF_REQURIED)):
parsed_arg.config = [constants.DEFAULT_CONFIG]
return parsed_arg
def _build_args(self, optionlist, parser=None):
parser = parser or self.parser
for val in optionlist:
opt = AVAILABLE_CLI_OPTIONS[val]
parser.add_argument(*opt.cli, dest=val, **opt.kwargs)
def _build_subcommands(self) -> None:
"""
Builds and attaches all subcommands.
:return: None
"""
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
from freqtrade.utils import start_create_userdir, start_download_data, start_list_exchanges
subparsers = self.parser.add_subparsers(dest='subparser')
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.')
backtesting_cmd.set_defaults(func=start_backtesting)
self._build_args(optionlist=ARGS_BACKTEST, parser=backtesting_cmd)
# Add edge subcommand
edge_cmd = subparsers.add_parser('edge', help='Edge module.')
edge_cmd.set_defaults(func=start_edge)
self._build_args(optionlist=ARGS_EDGE, parser=edge_cmd)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.')
hyperopt_cmd.set_defaults(func=start_hyperopt)
self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd)
create_userdir_cmd = subparsers.add_parser('create-userdir',
help="Create user-data directory.")
create_userdir_cmd.set_defaults(func=start_create_userdir)
self._build_args(optionlist=ARGS_CREATE_USERDIR, parser=create_userdir_cmd)
# Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser(
'list-exchanges',
help='Print available exchanges.'
)
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
# Add download-data subcommand
download_data_cmd = subparsers.add_parser(
'download-data',
help='Download backtesting data.'
)
download_data_cmd.set_defaults(func=start_download_data)
self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd)

View File

@@ -0,0 +1,47 @@
import logging
from typing import Any, Dict
from freqtrade import OperationalException
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason,
is_exchange_available, is_exchange_bad,
is_exchange_officially_supported)
logger = logging.getLogger(__name__)
def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
"""
Check if the exchange name in the config file is supported by Freqtrade
:param check_for_bad: if True, check the exchange against the list of known 'bad'
exchanges
:return: False if exchange is 'bad', i.e. is known to work with the bot with
critical issues or does not work at all, crashes, etc. True otherwise.
raises an exception if the exchange if not supported by ccxt
and thus is not known for the Freqtrade at all.
"""
logger.info("Checking exchange...")
exchange = config.get('exchange', {}).get('name').lower()
if not is_exchange_available(exchange):
raise OperationalException(
f'Exchange "{exchange}" is not supported by ccxt '
f'and therefore not available for the bot.\n'
f'The following exchanges are supported by ccxt: '
f'{", ".join(available_exchanges())}'
)
if check_for_bad and is_exchange_bad(exchange):
raise OperationalException(f'Exchange "{exchange}" is known to not work with the bot yet. '
f'Reason: {get_exchange_bad_reason(exchange)}')
if is_exchange_officially_supported(exchange):
logger.info(f'Exchange "{exchange}" is officially supported '
f'by the Freqtrade development team.')
else:
logger.warning(f'Exchange "{exchange}" is supported by ccxt '
f'and therefore available for the bot but not officially supported '
f'by the Freqtrade development team. '
f'It may work flawlessly (please report back) or have serious issues. '
f'Use it at your own discretion.')
return True

View File

@@ -0,0 +1,319 @@
"""
Definition of cli arguments used in arguments.py
"""
import argparse
import os
from freqtrade import __version__, constants
def check_int_positive(value: str) -> int:
try:
uint = int(value)
if uint <= 0:
raise ValueError
except ValueError:
raise argparse.ArgumentTypeError(
f"{value} is invalid for this parameter, should be a positive integer value"
)
return uint
class Arg:
# Optional CLI arguments
def __init__(self, *args, **kwargs):
self.cli = args
self.kwargs = kwargs
# List of available command line options
AVAILABLE_CLI_OPTIONS = {
# Common options
"verbosity": Arg(
'-v', '--verbose',
help='Verbose mode (-vv for more, -vvv to get all messages).',
action='count',
default=0,
),
"logfile": Arg(
'--logfile',
help='Log to the file specified.',
metavar='FILE',
),
"version": Arg(
'-V', '--version',
action='version',
version=f'%(prog)s {__version__}',
),
"config": Arg(
'-c', '--config',
help=f'Specify configuration file (default: `{constants.DEFAULT_CONFIG}`). '
f'Multiple --config options may be used. '
f'Can be set to `-` to read config from stdin.',
action='append',
metavar='PATH',
),
"datadir": Arg(
'-d', '--datadir',
help='Path to directory with historical backtesting data.',
metavar='PATH',
),
"user_data_dir": Arg(
'--userdir', '--user-data-dir',
help='Path to userdata directory.',
metavar='PATH',
),
# Main options
"strategy": Arg(
'-s', '--strategy',
help='Specify strategy class name (default: `%(default)s`).',
metavar='NAME',
default='DefaultStrategy',
),
"strategy_path": Arg(
'--strategy-path',
help='Specify additional strategy lookup path.',
metavar='PATH',
),
"db_url": Arg(
'--db-url',
help=f'Override trades database URL, this is useful in custom deployments '
f'(default: `{constants.DEFAULT_DB_PROD_URL}` for Live Run mode, '
f'`{constants.DEFAULT_DB_DRYRUN_URL}` for Dry Run).',
metavar='PATH',
),
"sd_notify": Arg(
'--sd-notify',
help='Notify systemd service manager.',
action='store_true',
),
# Optimize common
"ticker_interval": Arg(
'-i', '--ticker-interval',
help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).',
),
"timerange": Arg(
'--timerange',
help='Specify what timerange of data to use.',
),
"max_open_trades": Arg(
'--max_open_trades',
help='Specify max_open_trades to use.',
type=int,
metavar='INT',
),
"stake_amount": Arg(
'--stake_amount',
help='Specify stake_amount.',
type=float,
),
"refresh_pairs": Arg(
'-r', '--refresh-pairs-cached',
help='Refresh the pairs files in tests/testdata with the latest data from the '
'exchange. Use it if you want to run your optimization commands with '
'up-to-date data.',
action='store_true',
),
# Backtesting
"position_stacking": Arg(
'--eps', '--enable-position-stacking',
help='Allow buying the same pair multiple times (position stacking).',
action='store_true',
default=False,
),
"use_max_market_positions": Arg(
'--dmmp', '--disable-max-market-positions',
help='Disable applying `max_open_trades` during backtest '
'(same as setting `max_open_trades` to a very high number).',
action='store_false',
default=True,
),
"strategy_list": Arg(
'--strategy-list',
help='Provide a space-separated list of strategies to backtest. '
'Please note that ticker-interval needs to be set either in config '
'or via command line. When using this together with `--export trades`, '
'the strategy-name is injected into the filename '
'(so `backtest-data.json` becomes `backtest-data-DefaultStrategy.json`',
nargs='+',
),
"export": Arg(
'--export',
help='Export backtest results, argument are: trades. '
'Example: `--export=trades`',
),
"exportfilename": Arg(
'--export-filename',
help='Save backtest results to the file with this filename (default: `%(default)s`). '
'Requires `--export` to be set as well. '
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
metavar='PATH',
default=os.path.join('user_data', 'backtest_results',
'backtest-result.json'),
),
# Edge
"stoploss_range": Arg(
'--stoplosses',
help='Defines a range of stoploss values against which edge will assess the strategy. '
'The format is "min,max,step" (without any space). '
'Example: `--stoplosses=-0.01,-0.1,-0.001`',
),
# Hyperopt
"hyperopt": Arg(
'--customhyperopt',
help='Specify hyperopt class name (default: `%(default)s`).',
metavar='NAME',
default=constants.DEFAULT_HYPEROPT,
),
"hyperopt_path": Arg(
'--hyperopt-path',
help='Specify additional lookup path for Hyperopts and Hyperopt Loss functions.',
metavar='PATH',
),
"epochs": Arg(
'-e', '--epochs',
help='Specify number of epochs (default: %(default)d).',
type=check_int_positive,
metavar='INT',
default=constants.HYPEROPT_EPOCH,
),
"spaces": Arg(
'-s', '--spaces',
help='Specify which parameters to hyperopt. Space-separated list. '
'Default: `%(default)s`.',
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
nargs='+',
default='all',
),
"print_all": Arg(
'--print-all',
help='Print all results, not only the best ones.',
action='store_true',
default=False,
),
"print_colorized": Arg(
'--no-color',
help='Disable colorization of hyperopt results. May be useful if you are '
'redirecting output to a file.',
action='store_false',
default=True,
),
"print_json": Arg(
'--print-json',
help='Print best result detailization in JSON format.',
action='store_true',
default=False,
),
"hyperopt_jobs": Arg(
'-j', '--job-workers',
help='The number of concurrently running jobs for hyperoptimization '
'(hyperopt worker processes). '
'If -1 (default), all CPUs are used, for -2, all CPUs but one are used, etc. '
'If 1 is given, no parallel computing code is used at all.',
type=int,
metavar='JOBS',
default=-1,
),
"hyperopt_random_state": Arg(
'--random-state',
help='Set random state to some positive integer for reproducible hyperopt results.',
type=check_int_positive,
metavar='INT',
),
"hyperopt_min_trades": Arg(
'--min-trades',
help="Set minimal desired number of trades for evaluations in the hyperopt "
"optimization path (default: 1).",
type=check_int_positive,
metavar='INT',
default=1,
),
"hyperopt_continue": Arg(
"--continue",
help="Continue hyperopt from previous runs. "
"By default, temporary files will be removed and hyperopt will start from scratch.",
default=False,
action='store_true',
),
"hyperopt_loss": Arg(
'--hyperopt-loss',
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
'Different functions can generate completely different results, '
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.'
'(default: `%(default)s`).',
metavar='NAME',
default=constants.DEFAULT_HYPEROPT_LOSS,
),
# List exchanges
"print_one_column": Arg(
'-1', '--one-column',
help='Print exchanges in one column.',
action='store_true',
),
# Script options
"pairs": Arg(
'-p', '--pairs',
help='Show profits for only these pairs. Pairs are space-separated.',
nargs='+',
),
# Download data
"pairs_file": Arg(
'--pairs-file',
help='File containing a list of pairs to download.',
metavar='FILE',
),
"days": Arg(
'--days',
help='Download data for given number of days.',
type=check_int_positive,
metavar='INT',
),
"exchange": Arg(
'--exchange',
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
f'Only valid if no config is provided.',
),
"timeframes": Arg(
'-t', '--timeframes',
help=f'Specify which tickers to download. Space-separated list. '
f'Default: `1m 5m`.',
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
'6h', '8h', '12h', '1d', '3d', '1w'],
default=['1m', '5m'],
nargs='+',
),
"erase": Arg(
'--erase',
help='Clean all existing data for the selected exchange/pairs/timeframes.',
action='store_true',
),
# Plot dataframe
"indicators1": Arg(
'--indicators1',
help='Set indicators from your strategy you want in the first row of the graph. '
'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.',
default='sma,ema3,ema5',
),
"indicators2": Arg(
'--indicators2',
help='Set indicators from your strategy you want in the third row of the graph. '
'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.',
default='macd,macdsignal',
),
"plot_limit": Arg(
'--plot-limit',
help='Specify tick limit for plotting. Notice: too high values cause huge files. '
'Default: %(default)s.',
type=check_int_positive,
metavar='INT',
default=750,
),
"trade_source": Arg(
'--trade-source',
help='Specify the source for trades (Can be DB or file (backtest file)) '
'Default: %(default)s',
choices=["DB", "file"],
default="file",
),
}

View File

@@ -0,0 +1,113 @@
import logging
from typing import Any, Dict
from jsonschema import Draft4Validator, validators
from jsonschema.exceptions import ValidationError, best_match
from freqtrade import constants, OperationalException
logger = logging.getLogger(__name__)
def _extend_validator(validator_class):
"""
Extended validator for the Freqtrade configuration JSON Schema.
Currently it only handles defaults for subschemas.
"""
validate_properties = validator_class.VALIDATORS['properties']
def set_defaults(validator, properties, instance, schema):
for prop, subschema in properties.items():
if 'default' in subschema:
instance.setdefault(prop, subschema['default'])
for error in validate_properties(
validator, properties, instance, schema,
):
yield error
return validators.extend(
validator_class, {'properties': set_defaults}
)
FreqtradeValidator = _extend_validator(Draft4Validator)
def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate the configuration follow the Config Schema
:param conf: Config in JSON format
:return: Returns the config if valid, otherwise throw an exception
"""
try:
FreqtradeValidator(constants.CONF_SCHEMA).validate(conf)
return conf
except ValidationError as e:
logger.critical(
f"Invalid configuration. See config.json.example. Reason: {e}"
)
raise ValidationError(
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
)
def validate_config_consistency(conf: Dict[str, Any]) -> None:
"""
Validate the configuration consistency.
Should be ran after loading both configuration and strategy,
since strategies can set certain configuration settings too.
:param conf: Config in JSON format
:return: Returns None if everything is ok, otherwise throw an OperationalException
"""
# validating trailing stoploss
_validate_trailing_stoploss(conf)
_validate_edge(conf)
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
if conf.get('stoploss') == 0.0:
raise OperationalException(
'The config stoploss needs to be different from 0 to avoid problems with sell orders.'
)
# Skip if trailing stoploss is not activated
if not conf.get('trailing_stop', False):
return
tsl_positive = float(conf.get('trailing_stop_positive', 0))
tsl_offset = float(conf.get('trailing_stop_positive_offset', 0))
tsl_only_offset = conf.get('trailing_only_offset_is_reached', False)
if tsl_only_offset:
if tsl_positive == 0.0:
raise OperationalException(
'The config trailing_only_offset_is_reached needs '
'trailing_stop_positive_offset to be more than 0 in your config.')
if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive:
raise OperationalException(
'The config trailing_stop_positive_offset needs '
'to be greater than trailing_stop_positive in your config.')
# Fetch again without default
if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0:
raise OperationalException(
'The config trailing_stop_positive needs to be different from 0 '
'to avoid problems with sell orders.'
)
def _validate_edge(conf: Dict[str, Any]) -> None:
"""
Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists.
"""
if not conf.get('edge', {}).get('enabled'):
return
if conf.get('pairlist', {}).get('method') == 'VolumePairList':
raise OperationalException(
"Edge and VolumePairList are incompatible, "
"Edge will override whatever pairs VolumePairlist selects."
)

View File

@@ -0,0 +1,371 @@
"""
This module contains the configuration class
"""
import logging
import warnings
from argparse import Namespace
from copy import deepcopy
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional
from freqtrade import OperationalException, constants
from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.config_validation import (
validate_config_consistency, validate_config_schema)
from freqtrade.configuration.directory_operations import (create_datadir,
create_userdata_dir)
from freqtrade.configuration.load_config import load_config_file
from freqtrade.loggers import setup_logging
from freqtrade.misc import deep_merge_dicts, json_load
from freqtrade.state import RunMode
logger = logging.getLogger(__name__)
class Configuration(object):
"""
Class to read and init the bot configuration
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
"""
def __init__(self, args: Namespace, runmode: RunMode = None) -> None:
self.args = args
self.config: Optional[Dict[str, Any]] = None
self.runmode = runmode
def get_config(self) -> Dict[str, Any]:
"""
Return the config. Use this method to get the bot config
:return: Dict: Bot config
"""
if self.config is None:
self.config = self.load_config()
return self.config
@staticmethod
def from_files(files: List[str]) -> Dict[str, Any]:
"""
Iterate through the config files passed in, loading all of them
and merging their contents.
Files are loaded in sequence, parameters in later configuration files
override the same parameter from an earlier file (last definition wins).
:param files: List of file paths
:return: configuration dictionary
"""
# Keep this method as staticmethod, so it can be used from interactive environments
config: Dict[str, Any] = {}
if not files:
return deepcopy(constants.MINIMAL_CONFIG)
# We expect here a list of config filenames
for path in files:
logger.info(f'Using config: {path} ...')
# Merge config options, overwriting old values
config = deep_merge_dicts(load_config_file(path), config)
# Normalize config
if 'internals' not in config:
config['internals'] = {}
# validate configuration before returning
logger.info('Validating configuration ...')
validate_config_schema(config)
return config
def load_config(self) -> Dict[str, Any]:
"""
Extract information for sys.argv and load the bot configuration
:return: Configuration dictionary
"""
# Load all configs
config: Dict[str, Any] = Configuration.from_files(self.args.config)
self._process_common_options(config)
self._process_optimize_options(config)
self._process_plot_options(config)
self._process_runmode(config)
# Check if the exchange set by the user is supported
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
self._resolve_pairs_list(config)
validate_config_consistency(config)
return config
def _process_logging_options(self, config: Dict[str, Any]) -> None:
"""
Extract information for sys.argv and load logging configuration:
the -v/--verbose, --logfile options
"""
# Log level
if 'verbosity' in self.args and self.args.verbosity:
config.update({'verbosity': self.args.verbosity})
else:
config.update({'verbosity': 0})
if 'logfile' in self.args and self.args.logfile:
config.update({'logfile': self.args.logfile})
setup_logging(config)
def _process_common_options(self, config: Dict[str, Any]) -> None:
self._process_logging_options(config)
# Set strategy if not specified in config and or if it's non default
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'):
config.update({'strategy': self.args.strategy})
self._args_to_config(config, argname='strategy_path',
logstring='Using additional Strategy lookup path: {}')
if ('db_url' in self.args and self.args.db_url and
self.args.db_url != constants.DEFAULT_DB_PROD_URL):
config.update({'db_url': self.args.db_url})
logger.info('Parameter --db-url detected ...')
if config.get('dry_run', False):
logger.info('Dry run is enabled')
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
# Default to in-memory db for dry_run if not specified
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
else:
if not config.get('db_url', None):
config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled')
logger.info(f'Using DB: "{config["db_url"]}"')
if config.get('forcebuy_enable', False):
logger.warning('`forcebuy` RPC message enabled.')
# Setting max_open_trades to infinite if -1
if config.get('max_open_trades') == -1:
config['max_open_trades'] = float('inf')
# Support for sd_notify
if 'sd_notify' in self.args and self.args.sd_notify:
config['internals'].update({'sd_notify': True})
def _process_datadir_options(self, config: Dict[str, Any]) -> None:
"""
Extract information for sys.argv and load directory configurations
--user-data, --datadir
"""
# Check exchange parameter here - otherwise `datadir` might be wrong.
if "exchange" in self.args and self.args.exchange:
config['exchange']['name'] = self.args.exchange
logger.info(f"Using exchange {config['exchange']['name']}")
if 'user_data_dir' in self.args and self.args.user_data_dir:
config.update({'user_data_dir': self.args.user_data_dir})
elif 'user_data_dir' not in config:
# Default to cwd/user_data (legacy option ...)
config.update({'user_data_dir': str(Path.cwd() / "user_data")})
# reset to user_data_dir so this contains the absolute path.
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
logger.info('Using user-data directory: %s ...', config['user_data_dir'])
if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': create_datadir(config, self.args.datadir)})
else:
config.update({'datadir': create_datadir(config, None)})
logger.info('Using data directory: %s ...', config.get('datadir'))
def _process_optimize_options(self, config: Dict[str, Any]) -> None:
# This will override the strategy configuration
self._args_to_config(config, argname='ticker_interval',
logstring='Parameter -i/--ticker-interval detected ... '
'Using ticker_interval: {} ...')
self._args_to_config(config, argname='position_stacking',
logstring='Parameter --enable-position-stacking detected ...')
if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions:
config.update({'use_max_market_positions': False})
logger.info('Parameter --disable-max-market-positions detected ...')
logger.info('max_open_trades set to unlimited ...')
elif 'max_open_trades' in self.args and self.args.max_open_trades:
config.update({'max_open_trades': self.args.max_open_trades})
logger.info('Parameter --max_open_trades detected, '
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
else:
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
self._args_to_config(config, argname='stake_amount',
logstring='Parameter --stake_amount detected, '
'overriding stake_amount to: {} ...')
self._args_to_config(config, argname='timerange',
logstring='Parameter --timerange detected: {} ...')
self._process_datadir_options(config)
self._args_to_config(config, argname='refresh_pairs',
logstring='Parameter -r/--refresh-pairs-cached detected ...',
deprecated_msg='-r/--refresh-pairs-cached will be removed soon.')
self._args_to_config(config, argname='strategy_list',
logstring='Using strategy list of {} Strategies', logfun=len)
self._args_to_config(config, argname='ticker_interval',
logstring='Overriding ticker interval with Command line argument')
self._args_to_config(config, argname='export',
logstring='Parameter --export detected: {} ...')
self._args_to_config(config, argname='exportfilename',
logstring='Storing backtest results to {} ...')
# Edge section:
if 'stoploss_range' in self.args and self.args.stoploss_range:
txt_range = eval(self.args.stoploss_range)
config['edge'].update({'stoploss_range_min': txt_range[0]})
config['edge'].update({'stoploss_range_max': txt_range[1]})
config['edge'].update({'stoploss_range_step': txt_range[2]})
logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range)
# Hyperopt section
self._args_to_config(config, argname='hyperopt',
logstring='Using Hyperopt file {}')
self._args_to_config(config, argname='hyperopt_path',
logstring='Using additional Hyperopt lookup path: {}')
self._args_to_config(config, argname='epochs',
logstring='Parameter --epochs detected ... '
'Will run Hyperopt with for {} epochs ...'
)
self._args_to_config(config, argname='spaces',
logstring='Parameter -s/--spaces detected: {}')
self._args_to_config(config, argname='print_all',
logstring='Parameter --print-all detected ...')
if 'print_colorized' in self.args and not self.args.print_colorized:
logger.info('Parameter --no-color detected ...')
config.update({'print_colorized': False})
else:
config.update({'print_colorized': True})
self._args_to_config(config, argname='print_json',
logstring='Parameter --print-json detected ...')
self._args_to_config(config, argname='hyperopt_jobs',
logstring='Parameter -j/--job-workers detected: {}')
self._args_to_config(config, argname='hyperopt_random_state',
logstring='Parameter --random-state detected: {}')
self._args_to_config(config, argname='hyperopt_min_trades',
logstring='Parameter --min-trades detected: {}')
self._args_to_config(config, argname='hyperopt_continue',
logstring='Hyperopt continue: {}')
self._args_to_config(config, argname='hyperopt_loss',
logstring='Using loss function: {}')
def _process_plot_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='pairs',
logstring='Using pairs {}')
self._args_to_config(config, argname='indicators1',
logstring='Using indicators1: {}')
self._args_to_config(config, argname='indicators2',
logstring='Using indicators2: {}')
self._args_to_config(config, argname='plot_limit',
logstring='Limiting plot to: {}')
self._args_to_config(config, argname='trade_source',
logstring='Using trades from: {}')
self._args_to_config(config, argname='erase',
logstring='Erase detected. Deleting existing data.')
self._args_to_config(config, argname='timeframes',
logstring='timeframes --timeframes: {}')
self._args_to_config(config, argname='days',
logstring='Detected --days: {}')
def _process_runmode(self, config: Dict[str, Any]) -> None:
if not self.runmode:
# Handle real mode, infer dry/live from config
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
logger.info(f"Runmode set to {self.runmode}.")
config.update({'runmode': self.runmode})
def _args_to_config(self, config: Dict[str, Any], argname: str,
logstring: str, logfun: Optional[Callable] = None,
deprecated_msg: Optional[str] = None) -> None:
"""
:param config: Configuration dictionary
:param argname: Argumentname in self.args - will be copied to config dict.
:param logstring: Logging String
:param logfun: logfun is applied to the configuration entry before passing
that entry to the log string using .format().
sample: logfun=len (prints the length of the found
configuration instead of the content)
"""
if argname in self.args and getattr(self.args, argname):
config.update({argname: getattr(self.args, argname)})
if logfun:
logger.info(logstring.format(logfun(config[argname])))
else:
logger.info(logstring.format(config[argname]))
if deprecated_msg:
warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning)
def _resolve_pairs_list(self, config: Dict[str, Any]) -> None:
"""
Helper for download script.
Takes first found:
* -p (pairs argument)
* --pairs-file
* whitelist from config
"""
if "pairs" in config:
return
if "pairs_file" in self.args and self.args.pairs_file:
pairs_file = Path(self.args.pairs_file)
logger.info(f'Reading pairs file "{pairs_file}".')
# Download pairs from the pairs file if no config is specified
# or if pairs file is specified explicitely
if not pairs_file.exists():
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
with pairs_file.open('r') as f:
config['pairs'] = json_load(f)
config['pairs'].sort()
return
if "config" in self.args and self.args.config:
logger.info("Using pairlist from configuration.")
config['pairs'] = config.get('exchange', {}).get('pair_whitelist')
else:
# Fall back to /dl_path/pairs.json
pairs_file = Path(config['datadir']) / "pairs.json"
if pairs_file.exists():
with pairs_file.open('r') as f:
config['pairs'] = json_load(f)
if 'pairs' in config:
config['pairs'].sort()

View File

@@ -0,0 +1,50 @@
import logging
from typing import Any, Dict, Optional
from pathlib import Path
from freqtrade import OperationalException
logger = logging.getLogger(__name__)
def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str:
folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data")
if not datadir:
# set datadir
exchange_name = config.get('exchange', {}).get('name').lower()
folder = folder.joinpath(exchange_name)
if not folder.is_dir():
folder.mkdir(parents=True)
logger.info(f'Created data directory: {datadir}')
return str(folder)
def create_userdata_dir(directory: str, create_dir=False) -> Path:
"""
Create userdata directory structure.
if create_dir is True, then the parent-directory will be created if it does not exist.
Sub-directories will always be created if the parent directory exists.
Raises OperationalException if given a non-existing directory.
:param directory: Directory to check
:param create_dir: Create directory if it does not exist.
:return: Path object containing the directory
"""
sub_dirs = ["backtest_results", "data", "hyperopts", "hyperopt_results", "plot", "strategies", ]
folder = Path(directory)
if not folder.is_dir():
if create_dir:
folder.mkdir(parents=True)
logger.info(f'Created user-data directory: {folder}')
else:
raise OperationalException(
f"Directory `{folder}` does not exist. "
"Please use `freqtrade create-userdir` to create a user directory")
# Create required subdirectories
for f in sub_dirs:
subfolder = folder / f
if not subfolder.is_dir():
subfolder.mkdir(parents=False)
return folder

View File

@@ -0,0 +1,33 @@
"""
This module contain functions to load the configuration file
"""
import rapidjson
import logging
import sys
from typing import Any, Dict
from freqtrade import OperationalException
logger = logging.getLogger(__name__)
CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS
def load_config_file(path: str) -> Dict[str, Any]:
"""
Loads a config file from the given path
:param path: path as str
:return: configuration as dictionary
"""
try:
# Read config from stdin if requested in the options
with open(path) if path != '-' else sys.stdin as file:
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
except FileNotFoundError:
raise OperationalException(
f'Config file "{path}" not found!'
' Please create a config file or check whether it exists.')
return config

View File

@@ -0,0 +1,70 @@
"""
This module contains the argument manager class
"""
import re
from typing import Optional
import arrow
class TimeRange():
"""
object defining timerange inputs.
[start/stop]type defines if [start/stop]ts shall be used.
if *type is None, don't use corresponding startvalue.
"""
def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None,
startts: int = 0, stopts: int = 0):
self.starttype: Optional[str] = starttype
self.stoptype: Optional[str] = stoptype
self.startts: int = startts
self.stopts: int = stopts
def __eq__(self, other):
"""Override the default Equals behavior"""
return (self.starttype == other.starttype and self.stoptype == other.stoptype
and self.startts == other.startts and self.stopts == other.stopts)
@staticmethod
def parse_timerange(text: Optional[str]):
"""
Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange
:return: Start and End range period
"""
if text is None:
return TimeRange(None, None, 0, 0)
syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
for rex, stype in syntax:
# Apply the regular expression to text
match = re.match(rex, text)
if match: # Regex has matched
rvals = match.groups()
index = 0
start: int = 0
stop: int = 0
if stype[0]:
starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8:
start = arrow.get(starts, 'YYYYMMDD').timestamp
else:
start = int(starts)
index += 1
if stype[1]:
stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8:
stop = arrow.get(stops, 'YYYYMMDD').timestamp
else:
stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop)
raise Exception('Incorrect syntax for timerange "%s"' % text)

View File

@@ -5,13 +5,13 @@ bot constants
""" """
DEFAULT_CONFIG = 'config.json' DEFAULT_CONFIG = 'config.json'
DEFAULT_EXCHANGE = 'bittrex' DEFAULT_EXCHANGE = 'bittrex'
DYNAMIC_WHITELIST = 20 # pairs
PROCESS_THROTTLE_SECS = 5 # sec PROCESS_THROTTLE_SECS = 5 # sec
DEFAULT_TICKER_INTERVAL = 5 # min DEFAULT_TICKER_INTERVAL = 5 # min
HYPEROPT_EPOCH = 100 # epochs HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy' DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_HYPEROPT = 'DefaultHyperOpts' DEFAULT_HYPEROPT = 'DefaultHyperOpts'
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://' DEFAULT_DB_DRYRUN_URL = 'sqlite://'
UNLIMITED_STAKE_AMOUNT = 'unlimited' UNLIMITED_STAKE_AMOUNT = 'unlimited'
@@ -22,7 +22,6 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
DRY_RUN_WALLET = 999.9 DRY_RUN_WALLET = 999.9
DEFAULT_DOWNLOAD_TICKER_INTERVALS = '1m 5m'
TICKER_INTERVALS = [ TICKER_INTERVALS = [
'1m', '3m', '5m', '15m', '30m', '1m', '3m', '5m', '15m', '30m',
@@ -38,6 +37,20 @@ SUPPORTED_FIAT = [
"BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT" "BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT"
] ]
MINIMAL_CONFIG = {
'stake_currency': '',
'dry_run': True,
'exchange': {
'name': '',
'key': '',
'secret': '',
'pair_whitelist': [],
'ccxt_async_config': {
'enableRateLimit': True,
}
}
}
# Required json-schema for user specified config # Required json-schema for user specified config
CONF_SCHEMA = { CONF_SCHEMA = {
'type': 'object', 'type': 'object',

View File

@@ -3,6 +3,7 @@ Helpers when analyzing backtest data
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@@ -29,7 +30,7 @@ def load_backtest_data(filename) -> pd.DataFrame:
filename = Path(filename) filename = Path(filename)
if not filename.is_file(): if not filename.is_file():
raise ValueError("File {filename} does not exist.") raise ValueError(f"File {filename} does not exist.")
with filename.open() as file: with filename.open() as file:
data = json_load(file) data = json_load(file)
@@ -66,7 +67,6 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int
dates = pd.Series(pd.concat(dates).values, name='date') dates = pd.Series(pd.concat(dates).values, name='date')
df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns)
df2 = df2.astype(dtype={"open_time": "datetime64", "close_time": "datetime64"})
df2 = pd.concat([dates, df2], axis=1) df2 = pd.concat([dates, df2], axis=1)
df2 = df2.set_index('date') df2 = df2.set_index('date')
df_final = df2.resample(freq)[['pair']].count() df_final = df2.resample(freq)[['pair']].count()
@@ -81,19 +81,30 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
""" """
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
persistence.init(db_url, clean_open_orders=False) persistence.init(db_url, clean_open_orders=False)
columns = ["pair", "profit", "open_time", "close_time",
"open_rate", "close_rate", "duration", "sell_reason",
"max_rate", "min_rate"]
trades = pd.DataFrame([(t.pair, t.calc_profit(), columns = ["pair", "open_time", "close_time", "profit", "profitperc",
"open_rate", "close_rate", "amount", "duration", "sell_reason",
"fee_open", "fee_close", "open_rate_requested", "close_rate_requested",
"stake_amount", "max_rate", "min_rate", "id", "exchange",
"stop_loss", "initial_stop_loss", "strategy", "ticker_interval"]
trades = pd.DataFrame([(t.pair,
t.open_date.replace(tzinfo=pytz.UTC), t.open_date.replace(tzinfo=pytz.UTC),
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None,
t.open_rate, t.close_rate, t.calc_profit(), t.calc_profit_percent(),
t.close_date.timestamp() - t.open_date.timestamp() t.open_rate, t.close_rate, t.amount,
if t.close_date else None, (t.close_date.timestamp() - t.open_date.timestamp()
if t.close_date else None),
t.sell_reason, t.sell_reason,
t.fee_open, t.fee_close,
t.open_rate_requested,
t.close_rate_requested,
t.stake_amount,
t.max_rate, t.max_rate,
t.min_rate, t.min_rate,
t.id, t.exchange,
t.stop_loss, t.initial_stop_loss,
t.strategy, t.ticker_interval
) )
for t in Trade.query.all()], for t in Trade.query.all()],
columns=columns) columns=columns)
@@ -101,6 +112,18 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
return trades return trades
def load_trades(config) -> pd.DataFrame:
"""
Based on configuration option "trade_source":
* loads data from DB (using `db_url`)
* loads data from backtestfile (using `exportfilename`)
"""
if config["trade_source"] == "DB":
return load_trades_from_db(config["db_url"])
elif config["trade_source"] == "file":
return load_backtest_data(Path(config["exportfilename"]))
def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame:
""" """
Compare trades and backtested pair DataFrames to get trades performed on backtested period Compare trades and backtested pair DataFrames to get trades performed on backtested period
@@ -109,3 +132,34 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p
trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) & trades = trades.loc[(trades['open_time'] >= dataframe.iloc[0]['date']) &
(trades['close_time'] <= dataframe.iloc[-1]['date'])] (trades['close_time'] <= dataframe.iloc[-1]['date'])]
return trades return trades
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "close"):
"""
Combine multiple dataframes "column"
:param tickers: Dict of Dataframes, dict key should be pair.
:param column: Column in the original dataframes to use
:return: DataFrame with the column renamed to the dict key, and a column
named mean, containing the mean of all pairs.
"""
df_comb = pd.concat([tickers[pair].set_index('date').rename(
{column: pair}, axis=1)[pair] for pair in tickers], axis=1)
df_comb['mean'] = df_comb.mean(axis=1)
return df_comb
def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame:
"""
Adds a column `col_name` with the cumulative profit for the given trades array.
:param df: DataFrame with date index
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
:return: Returns df with one additional column, col_name, containing the cumulative profit.
"""
df[col_name] = trades.set_index('close_time')['profitperc'].cumsum()
# Set first value to 0
df.loc[df.iloc[0].name, col_name] = 0
# FFill to get continuous
df[col_name] = df[col_name].ffill()
return df

View File

@@ -17,7 +17,7 @@ from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DataProvider(object): class DataProvider():
def __init__(self, config: dict, exchange: Exchange) -> None: def __init__(self, config: dict, exchange: Exchange) -> None:
self._config = config self._config = config
@@ -44,36 +44,49 @@ class DataProvider(object):
def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame:
""" """
get ohlcv data for the given pair as DataFrame Get ohlcv data for the given pair as DataFrame
Please check `available_pairs` to verify which pairs are currently cached. Please use the `available_pairs` method to verify which pairs are currently cached.
:param pair: pair to get the data for :param pair: pair to get the data for
:param ticker_interval: ticker_interval to get pair for :param ticker_interval: ticker interval to get data for
:param copy: copy dataframe before returning. :param copy: copy dataframe before returning if True.
Use false only for RO operations (where the dataframe is not modified) Use False only for read-only operations (where the dataframe is not modified)
""" """
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
if ticker_interval: return self._exchange.klines((pair, ticker_interval or self._config['ticker_interval']),
pairtick = (pair, ticker_interval) copy=copy)
else:
pairtick = (pair, self._config['ticker_interval'])
return self._exchange.klines(pairtick, copy=copy)
else: else:
return DataFrame() return DataFrame()
def historic_ohlcv(self, pair: str, ticker_interval: str) -> DataFrame: def historic_ohlcv(self, pair: str, ticker_interval: str = None) -> DataFrame:
""" """
get stored historic ohlcv data Get stored historic ohlcv data
:param pair: pair to get the data for :param pair: pair to get the data for
:param ticker_interval: ticker_interval to get pair for :param ticker_interval: ticker interval to get data for
""" """
return load_pair_history(pair=pair, return load_pair_history(pair=pair,
ticker_interval=ticker_interval, ticker_interval=ticker_interval or self._config['ticker_interval'],
refresh_pairs=False, refresh_pairs=False,
datadir=Path(self._config['datadir']) if self._config.get( datadir=Path(self._config['datadir']) if self._config.get(
'datadir') else None 'datadir') else None
) )
def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame:
"""
Return pair ohlcv data, either live or cached historical -- depending
on the runmode.
:param pair: pair to get the data for
:param ticker_interval: ticker interval to get data for
"""
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
# Get live ohlcv data.
data = self.ohlcv(pair=pair, ticker_interval=ticker_interval)
else:
# Get historic ohlcv data (cached on disk).
data = self.historic_ohlcv(pair=pair, ticker_interval=ticker_interval)
if len(data) == 0:
logger.warning(f"No data found for ({pair}, {ticker_interval}).")
return data
def ticker(self, pair: str): def ticker(self, pair: str):
""" """
Return last ticker data Return last ticker data
@@ -81,11 +94,14 @@ class DataProvider(object):
# TODO: Implement me # TODO: Implement me
pass pass
def orderbook(self, pair: str, max: int): def orderbook(self, pair: str, maximum: int):
""" """
return latest orderbook data return latest orderbook data
: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.get_order_book(pair, max) return self._exchange.get_order_book(pair, maximum)
@property @property
def runmode(self) -> RunMode: def runmode(self) -> RunMode:

View File

@@ -16,7 +16,7 @@ import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade import OperationalException, misc from freqtrade import OperationalException, misc
from freqtrade.arguments import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.exchange import Exchange, timeframe_to_minutes from freqtrade.exchange import Exchange, timeframe_to_minutes
@@ -43,7 +43,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
start_index += 1 start_index += 1
if timerange.stoptype == 'line': if timerange.stoptype == 'line':
start_index = len(tickerlist) + timerange.stopts start_index = max(len(tickerlist) + timerange.stopts, 0)
if timerange.stoptype == 'index': if timerange.stoptype == 'index':
stop_index = timerange.stopts stop_index = timerange.stopts
elif timerange.stoptype == 'date': elif timerange.stoptype == 'date':
@@ -57,10 +57,8 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
return tickerlist[start_index:stop_index] return tickerlist[start_index:stop_index]
def load_tickerdata_file( def load_tickerdata_file(datadir: Optional[Path], pair: str, ticker_interval: str,
datadir: Optional[Path], pair: str, timerange: Optional[TimeRange] = None) -> Optional[list]:
ticker_interval: str,
timerange: Optional[TimeRange] = None) -> Optional[list]:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json
:return: tickerlist or None if unsuccesful :return: tickerlist or None if unsuccesful
@@ -68,13 +66,22 @@ def load_tickerdata_file(
filename = pair_data_filename(datadir, pair, ticker_interval) filename = pair_data_filename(datadir, pair, ticker_interval)
pairdata = misc.file_load_json(filename) pairdata = misc.file_load_json(filename)
if not pairdata: if not pairdata:
return None return []
if timerange: if timerange:
pairdata = trim_tickerlist(pairdata, timerange) pairdata = trim_tickerlist(pairdata, timerange)
return pairdata return pairdata
def store_tickerdata_file(datadir: Optional[Path], pair: str,
ticker_interval: str, data: list, is_zip: bool = False):
"""
Stores tickerdata to file
"""
filename = pair_data_filename(datadir, pair, ticker_interval)
misc.file_dump_json(filename, data, is_zip=is_zip)
def load_pair_history(pair: str, def load_pair_history(pair: str,
ticker_interval: str, ticker_interval: str,
datadir: Optional[Path], datadir: Optional[Path],
@@ -122,7 +129,7 @@ def load_pair_history(pair: str,
else: else:
logger.warning( logger.warning(
f'No history data for pair: "{pair}", interval: {ticker_interval}. ' f'No history data for pair: "{pair}", interval: {ticker_interval}. '
'Use --refresh-pairs-cached option or download_backtest_data.py ' 'Use --refresh-pairs-cached option or `freqtrade download-data` '
'script to download the data' 'script to download the data'
) )
return None return None
@@ -177,11 +184,14 @@ def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str)
return filename return filename
def load_cached_data_for_updating(filename: Path, ticker_interval: str, def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_interval: str,
timerange: Optional[TimeRange]) -> Tuple[List[Any], timerange: Optional[TimeRange]) -> Tuple[List[Any],
Optional[int]]: Optional[int]]:
""" """
Load cached data and choose what part of the data should be updated Load cached data to download more data.
If timerange is passed in, checks wether data from an before the stored data will be downloaded.
If that's the case than what's available should be completely overwritten.
Only used by download_pair_history().
""" """
since_ms = None since_ms = None
@@ -195,12 +205,11 @@ def load_cached_data_for_updating(filename: Path, ticker_interval: str,
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
# read the cached file # read the cached file
if filename.is_file(): # Intentionally don't pass timerange in - since we need to load the full dataset.
with open(filename, "rt") as file: data = load_tickerdata_file(datadir, pair, ticker_interval)
data = misc.json_load(file) # remove the last item, could be incomplete candle
# remove the last item, could be incomplete candle if data:
if data: data.pop()
data.pop()
else: else:
data = [] data = []
@@ -239,29 +248,28 @@ def download_pair_history(datadir: Optional[Path],
) )
try: try:
filename = pair_data_filename(datadir, pair, ticker_interval)
logger.info( logger.info(
f'Download history data for pair: "{pair}", interval: {ticker_interval} ' f'Download history data for pair: "{pair}", interval: {ticker_interval} '
f'and store in {datadir}.' f'and store in {datadir}.'
) )
data, since_ms = load_cached_data_for_updating(filename, ticker_interval, timerange) data, since_ms = load_cached_data_for_updating(datadir, pair, ticker_interval, timerange)
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
# Default since_ms to 30 days if nothing is given # Default since_ms to 30 days if nothing is given
new_data = exchange.get_history(pair=pair, ticker_interval=ticker_interval, new_data = exchange.get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval,
since_ms=since_ms if since_ms since_ms=since_ms if since_ms
else else
int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000) int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000)
data.extend(new_data) data.extend(new_data)
logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
logger.debug("New End: %s", misc.format_ms_time(data[-1][0])) logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
misc.file_dump_json(filename, data) store_tickerdata_file(datadir, pair, ticker_interval, data=data)
return True return True
except Exception as e: except Exception as e:

View File

@@ -10,8 +10,7 @@ import utils_find_1st as utf1st
from pandas import DataFrame from pandas import DataFrame
from freqtrade import constants, OperationalException from freqtrade import constants, OperationalException
from freqtrade.arguments import Arguments from freqtrade.configuration import TimeRange
from freqtrade.arguments import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
@@ -76,7 +75,7 @@ class Edge():
self._stoploss_range_step self._stoploss_range_step
) )
self._timerange: TimeRange = Arguments.parse_timerange("%s-" % arrow.now().shift( self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift(
days=-1 * self._since_number_of_days).format('YYYYMMDD')) days=-1 * self._since_number_of_days).format('YYYYMMDD'))
self.fee = self.exchange.get_fee() self.fee = self.exchange.get_fee()

View File

@@ -1,10 +1,13 @@
from freqtrade.exchange.exchange import Exchange # noqa: F401 from freqtrade.exchange.exchange import Exchange # noqa: F401
from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401 from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
is_exchange_bad,
is_exchange_available, is_exchange_available,
is_exchange_officially_supported, is_exchange_officially_supported,
available_exchanges) available_exchanges)
from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401
timeframe_to_minutes, timeframe_to_minutes,
timeframe_to_msecs) timeframe_to_msecs,
timeframe_to_next_date,
timeframe_to_prev_date)
from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.kraken import Kraken # noqa: F401
from freqtrade.exchange.binance import Binance # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401

View File

@@ -6,7 +6,7 @@ import asyncio
import inspect import inspect
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime from datetime import datetime, timezone
from math import ceil, floor from math import ceil, floor
from random import randint from random import randint
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@@ -25,6 +25,11 @@ logger = logging.getLogger(__name__)
API_RETRY_COUNT = 4 API_RETRY_COUNT = 4
BAD_EXCHANGES = {
"bitmex": "Various reasons",
"bitstamp": "Does not provide history. "
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
}
def retrier_async(f): def retrier_async(f):
@@ -85,6 +90,9 @@ class Exchange(object):
it does basic validation whether the specified exchange and pairs are valid. it does basic validation whether the specified exchange and pairs are valid.
:return: None :return: None
""" """
self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None
self._config.update(config) self._config.update(config)
self._cached_ticker: Dict[str, Any] = {} self._cached_ticker: Dict[str, Any] = {}
@@ -117,9 +125,9 @@ class Exchange(object):
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle'] self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
# Initialize ccxt objects # Initialize ccxt objects
self._api: ccxt.Exchange = self._init_ccxt( self._api = self._init_ccxt(
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
self._api_async: ccxt_async.Exchange = self._init_ccxt( self._api_async = self._init_ccxt(
exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config')) exchange_config, ccxt_async, ccxt_kwargs=exchange_config.get('ccxt_async_config'))
logger.info('Using Exchange "%s"', self.name) logger.info('Using Exchange "%s"', self.name)
@@ -171,8 +179,10 @@ class Exchange(object):
try: try:
api = getattr(ccxt_module, name.lower())(ex_config) api = getattr(ccxt_module, name.lower())(ex_config)
except (KeyError, AttributeError): except (KeyError, AttributeError) as e:
raise OperationalException(f'Exchange {name} is not supported') raise OperationalException(f'Exchange {name} is not supported') from e
except ccxt.BaseError as e:
raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
self.set_sandbox(api, exchange_config, name) self.set_sandbox(api, exchange_config, name)
@@ -255,7 +265,7 @@ class Exchange(object):
if not self.markets: if not self.markets:
logger.warning('Unable to validate pairs (assuming they are correct).') logger.warning('Unable to validate pairs (assuming they are correct).')
# return return
for pair in pairs: for pair in pairs:
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
@@ -264,11 +274,35 @@ class Exchange(object):
raise OperationalException( raise OperationalException(
f'Pair {pair} is not available on {self.name}. ' f'Pair {pair} is not available on {self.name}. '
f'Please remove {pair} from your whitelist.') f'Please remove {pair} from your whitelist.')
elif self.markets[pair].get('info', {}).get('IsRestricted', False):
# Warn users about restricted pairs in whitelist.
# We cannot determine reliably if Users are affected.
logger.warning(f"Pair {pair} is restricted for some users on this exchange."
f"Please check if you are impacted by this restriction "
f"on the exchange and eventually remove {pair} from your whitelist.")
def get_valid_pair_combination(self, curr_1, curr_2) -> str:
"""
Get valid pair combination of curr_1 and curr_2 by trying both combinations.
"""
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
if pair in self.markets and self.markets[pair].get('active'):
return pair
raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
def validate_timeframes(self, timeframe: List[str]) -> None: def validate_timeframes(self, timeframe: List[str]) -> None:
""" """
Checks if ticker interval from config is a supported timeframe on the exchange Checks if ticker interval from config is a supported timeframe on the exchange
""" """
if not hasattr(self._api, "timeframes") or self._api.timeframes is None:
# If timeframes attribute is missing (or is None), the exchange probably
# has no fetchOHLCV method.
# Therefore we also show that.
raise OperationalException(
f"The ccxt library does not provide the list of timeframes "
f"for the exchange \"{self.name}\" and this exchange "
f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
timeframes = self._api.timeframes timeframes = self._api.timeframes
if timeframe not in timeframes: if timeframe not in timeframes:
raise OperationalException( raise OperationalException(
@@ -342,7 +376,7 @@ class Exchange(object):
'side': side, 'side': side,
'remaining': amount, 'remaining': amount,
'datetime': arrow.utcnow().isoformat(), 'datetime': arrow.utcnow().isoformat(),
'status': "open", 'status': "closed" if ordertype == "market" else "open",
'fee': None, 'fee': None,
"info": {} "info": {}
} }
@@ -364,7 +398,9 @@ class Exchange(object):
try: try:
# Set the precision for amount and price(rate) as accepted by the exchange # Set the precision for amount and price(rate) as accepted by the exchange
amount = self.symbol_amount_prec(pair, amount) amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None needs_price = (ordertype != 'market'
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
rate = self.symbol_price_prec(pair, rate) if needs_price else None
return self._api.create_order(pair, ordertype, side, return self._api.create_order(pair, ordertype, side,
amount, rate, params) amount, rate, params)
@@ -372,18 +408,18 @@ class Exchange(object):
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(
f'Insufficient funds to create {ordertype} {side} order on market {pair}.' f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' f'Tried to {side} amount {amount} at rate {rate}.'
f'Message: {e}') f'Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise DependencyException( raise DependencyException(
f'Could not create {ordertype} {side} order on market {pair}.' f'Could not create {ordertype} {side} order on market {pair}.'
f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).' f'Tried to {side} amount {amount} at rate {rate}.'
f'Message: {e}') f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
def buy(self, pair: str, ordertype: str, amount: float, def buy(self, pair: str, ordertype: str, amount: float,
rate: float, time_in_force) -> Dict: rate: float, time_in_force) -> Dict:
@@ -436,7 +472,7 @@ class Exchange(object):
order = self.create_order(pair, ordertype, 'sell', amount, rate, params) order = self.create_order(pair, ordertype, 'sell', amount, rate, params)
logger.info('stoploss limit order added for %s. ' logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s' % (pair, stop_price, rate)) 'stop price: %s. limit: %s', pair, stop_price, rate)
return order return order
@retrier @retrier
@@ -468,9 +504,9 @@ class Exchange(object):
return balances return balances
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_tickers(self) -> Dict: def get_tickers(self) -> Dict:
@@ -479,18 +515,18 @@ class Exchange(object):
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching tickers in batch.' f'Exchange {self._api.name} does not support fetching tickers in batch.'
f'Message: {e}') f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self._cached_ticker.keys(): if refresh or pair not in self._cached_ticker.keys():
try: try:
if pair not in self._api.markets: if pair not in self._api.markets or not self._api.markets[pair].get('active'):
raise DependencyException(f"Pair {pair} not available") raise DependencyException(f"Pair {pair} not available")
data = self._api.fetch_ticker(pair) data = self._api.fetch_ticker(pair)
try: try:
@@ -503,26 +539,31 @@ class Exchange(object):
return data return data
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
else: else:
logger.info("returning cached ticker-data for %s", pair) logger.info("returning cached ticker-data for %s", pair)
return self._cached_ticker[pair] return self._cached_ticker[pair]
def get_history(self, pair: str, ticker_interval: str, def get_historic_ohlcv(self, pair: str, ticker_interval: str,
since_ms: int) -> List: since_ms: int) -> List:
""" """
Gets candle history using asyncio and returns the list of candles. Gets candle history using asyncio and returns the list of candles.
Handles all async doing. Handles all async doing.
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
:param pair: Pair to download
:param ticker_interval: Interval to get
:param since_ms: Timestamp in milliseconds to get history from
:returns List of tickers
""" """
return asyncio.get_event_loop().run_until_complete( return asyncio.get_event_loop().run_until_complete(
self._async_get_history(pair=pair, ticker_interval=ticker_interval, self._async_get_historic_ohlcv(pair=pair, ticker_interval=ticker_interval,
since_ms=since_ms)) since_ms=since_ms))
async def _async_get_history(self, pair: str, async def _async_get_historic_ohlcv(self, pair: str,
ticker_interval: str, ticker_interval: str,
since_ms: int) -> List: since_ms: int) -> List:
one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit one_call = timeframe_to_msecs(ticker_interval) * self._ohlcv_candle_limit
logger.debug( logger.debug(
@@ -548,7 +589,10 @@ class Exchange(object):
def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]: def refresh_latest_ohlcv(self, pair_list: List[Tuple[str, str]]) -> List[Tuple[str, List]]:
""" """
Refresh in-memory ohlcv asyncronously and set `_klines` with the result Refresh in-memory ohlcv asynchronously and set `_klines` with the result
Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
:param pair_list: List of 2 element tuples containing pair, interval to refresh
:return: Returns a List of ticker-dataframes.
""" """
logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list)) logger.debug("Refreshing ohlcv data for %d pairs", len(pair_list))
@@ -596,7 +640,7 @@ class Exchange(object):
async def _async_get_candle_history(self, pair: str, ticker_interval: str, async def _async_get_candle_history(self, pair: str, ticker_interval: str,
since_ms: Optional[int] = None) -> Tuple[str, str, List]: since_ms: Optional[int] = None) -> Tuple[str, str, List]:
""" """
Asyncronously gets candle histories using fetch_ohlcv Asynchronously gets candle histories using fetch_ohlcv
returns tuple: (pair, ticker_interval, ohlcv_list) returns tuple: (pair, ticker_interval, ohlcv_list)
""" """
try: try:
@@ -626,12 +670,12 @@ class Exchange(object):
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical candlestick data.' f'Exchange {self._api.name} does not support fetching historical candlestick data.'
f'Message: {e}') f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(f'Could not load ticker history due to {e.__class__.__name__}. '
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}') f'Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch ticker data. Msg: {e}') raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e
@retrier @retrier
def cancel_order(self, order_id: str, pair: str) -> None: def cancel_order(self, order_id: str, pair: str) -> None:
@@ -642,28 +686,33 @@ class Exchange(object):
return self._api.cancel_order(order_id, pair) return self._api.cancel_order(order_id, pair)
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise InvalidOrderException( raise InvalidOrderException(
f'Could not cancel order. Message: {e}') f'Could not cancel order. Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_order(self, order_id: str, pair: str) -> Dict: def get_order(self, order_id: str, pair: str) -> Dict:
if self._config['dry_run']: if self._config['dry_run']:
order = self._dry_run_open_orders[order_id] try:
return order order = self._dry_run_open_orders[order_id]
return order
except KeyError as e:
# Gracefully handle errors with dry-run orders.
raise InvalidOrderException(
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
try: try:
return self._api.fetch_order(order_id, pair) return self._api.fetch_order(order_id, pair)
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise InvalidOrderException( raise InvalidOrderException(
f'Tried to get an invalid order (id: {order_id}). Message: {e}') f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}') f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_order_book(self, pair: str, limit: int = 100) -> dict: def get_order_book(self, pair: str, limit: int = 100) -> dict:
@@ -679,12 +728,12 @@ class Exchange(object):
except ccxt.NotSupported as e: except ccxt.NotSupported as e:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching order book.' f'Exchange {self._api.name} does not support fetching order book.'
f'Message: {e}') f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get order book due to {e.__class__.__name__}. Message: {e}') f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
@@ -694,16 +743,17 @@ class Exchange(object):
return [] return []
try: try:
# Allow 5s offset to catch slight time offsets (discovered in #1185) # Allow 5s offset to catch slight time offsets (discovered in #1185)
my_trades = self._api.fetch_my_trades(pair, since.timestamp() - 5) # since needs to be int in milliseconds
my_trades = self._api.fetch_my_trades(pair, int((since.timestamp() - 5) * 1000))
matched_trades = [trade for trade in my_trades if trade['order'] == order_id] matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
return matched_trades return matched_trades
except ccxt.NetworkError as e: except ccxt.NetworkError as e:
raise TemporaryError( raise TemporaryError(
f'Could not get trades due to networking error. Message: {e}') f'Could not get trades due to networking error. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
@retrier @retrier
def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1,
@@ -717,21 +767,25 @@ class Exchange(object):
price=price, takerOrMaker=taker_or_maker)['rate'] price=price, takerOrMaker=taker_or_maker)['rate']
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e) from e
def is_exchange_bad(exchange: str) -> bool: def is_exchange_bad(exchange_name: str) -> bool:
return exchange in ['bitmex'] return exchange_name in BAD_EXCHANGES
def is_exchange_available(exchange: str, ccxt_module=None) -> bool: def get_exchange_bad_reason(exchange_name: str) -> str:
return exchange in available_exchanges(ccxt_module) return BAD_EXCHANGES.get(exchange_name, "")
def is_exchange_officially_supported(exchange: str) -> bool: def is_exchange_available(exchange_name: str, ccxt_module=None) -> bool:
return exchange in ['bittrex', 'binance'] return exchange_name in available_exchanges(ccxt_module)
def is_exchange_officially_supported(exchange_name: str) -> bool:
return exchange_name in ['bittrex', 'binance']
def available_exchanges(ccxt_module=None) -> List[str]: def available_exchanges(ccxt_module=None) -> List[str]:
@@ -749,13 +803,45 @@ def timeframe_to_seconds(ticker_interval: str) -> int:
def timeframe_to_minutes(ticker_interval: str) -> int: def timeframe_to_minutes(ticker_interval: str) -> int:
""" """
Same as above, but returns minutes. Same as timeframe_to_seconds, but returns minutes.
""" """
return ccxt.Exchange.parse_timeframe(ticker_interval) // 60 return ccxt.Exchange.parse_timeframe(ticker_interval) // 60
def timeframe_to_msecs(ticker_interval: str) -> int: def timeframe_to_msecs(ticker_interval: str) -> int:
""" """
Same as above, but returns milliseconds. Same as timeframe_to_seconds, but returns milliseconds.
""" """
return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
"""
Use Timeframe and determine last possible candle.
:param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to utcnow()
:returns: date of previous candle (with utc timezone)
"""
if not date:
date = datetime.now(timezone.utc)
timeframe_secs = timeframe_to_seconds(timeframe)
# Get offset based on timerame_secs
offset = date.timestamp() % timeframe_secs
# Subtract seconds passed since last offset
new_timestamp = date.timestamp() - offset
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
"""
Use Timeframe and determine next candle.
:param timeframe: timeframe in string format (e.g. "5m")
:param date: date to use. Defaults to utcnow()
:returns: date of next candle (with utc timezone)
"""
prevdate = timeframe_to_prev_date(timeframe, date)
timeframe_secs = timeframe_to_seconds(timeframe)
# Add one interval to previous candle
new_timestamp = prevdate.timestamp() + timeframe_secs
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)

View File

@@ -16,11 +16,12 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exchange import timeframe_to_minutes from freqtrade.configuration import validate_config_consistency
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver
from freqtrade.state import State from freqtrade.state import State, RunMode
from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.strategy.interface import SellType, IStrategy
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@@ -51,6 +52,9 @@ class FreqtradeBot(object):
self.strategy: IStrategy = StrategyResolver(self.config).strategy self.strategy: IStrategy = StrategyResolver(self.config).strategy
# Check config consistency here since strategies can set certain options
validate_config_consistency(config)
self.rpc: RPCManager = RPCManager(self) self.rpc: RPCManager = RPCManager(self)
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
@@ -75,6 +79,12 @@ class FreqtradeBot(object):
persistence.init(self.config.get('db_url', None), persistence.init(self.config.get('db_url', None),
clean_open_orders=self.config.get('dry_run', False)) clean_open_orders=self.config.get('dry_run', False))
# Stoploss on exchange does not make sense, therefore we need to disable that.
if (self.dataprovider.runmode == RunMode.DRY_RUN and
self.strategy.order_types.get('stoploss_on_exchange', False)):
logger.info("Disabling stoploss_on_exchange during dry-run.")
self.strategy.order_types['stoploss_on_exchange'] = False
config['order_types']['stoploss_on_exchange'] = False
# Set initial bot state from config # Set initial bot state from config
initial_state = self.config.get('initial_state') initial_state = self.config.get('initial_state')
self.state = State[initial_state.upper()] if initial_state else State.STOPPED self.state = State[initial_state.upper()] if initial_state else State.STOPPED
@@ -99,13 +109,12 @@ class FreqtradeBot(object):
# Adjust stoploss if it was changed # Adjust stoploss if it was changed
Trade.stoploss_reinitialization(self.strategy.stoploss) Trade.stoploss_reinitialization(self.strategy.stoploss)
def process(self) -> bool: def process(self) -> None:
""" """
Queries the persistence layer for open trades and handles them, Queries the persistence layer for open trades and handles them,
otherwise a new trade is created. otherwise a new trade is created.
:return: True if one or more trades has been created or closed, False otherwise :return: True if one or more trades has been created or closed, False otherwise
""" """
state_changed = False
# Check whether markets have to be reloaded # Check whether markets have to be reloaded
self.exchange._reload_markets() self.exchange._reload_markets()
@@ -132,19 +141,17 @@ class FreqtradeBot(object):
# First process current opened trades # First process current opened trades
for trade in trades: for trade in trades:
state_changed |= self.process_maybe_execute_sell(trade) self.process_maybe_execute_sell(trade)
# Then looking for buy opportunities # Then looking for buy opportunities
if len(trades) < self.config['max_open_trades']: if len(trades) < self.config['max_open_trades']:
state_changed = self.process_maybe_execute_buy() self.process_maybe_execute_buy()
if 'unfilledtimeout' in self.config: if 'unfilledtimeout' in self.config:
# Check and handle any timed out open orders # Check and handle any timed out open orders
self.check_handle_timedout() self.check_handle_timedout()
Trade.session.flush() Trade.session.flush()
return state_changed
def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]):
""" """
Extend whitelist with pairs from open trades Extend whitelist with pairs from open trades
@@ -253,11 +260,12 @@ class FreqtradeBot(object):
amount_reserve_percent = max(amount_reserve_percent, 0.5) amount_reserve_percent = max(amount_reserve_percent, 0.5)
return min(min_stake_amounts) / amount_reserve_percent return min(min_stake_amounts) / amount_reserve_percent
def create_trade(self) -> bool: def create_trades(self) -> bool:
""" """
Checks the implemented trading indicator(s) for a randomly picked pair, Checks the implemented trading strategy for buy-signals, using the active pair whitelist.
if one pair triggers the buy_signal a new trade record gets created If a pair triggers the buy_signal a new trade record gets created.
:return: True if a trade object has been created and persisted, False otherwise Checks pairs as long as the open trade count is below `max_open_trades`.
:return: True if at least one trade has been created.
""" """
interval = self.strategy.ticker_interval interval = self.strategy.ticker_interval
whitelist = copy.deepcopy(self.active_pair_whitelist) whitelist = copy.deepcopy(self.active_pair_whitelist)
@@ -276,15 +284,19 @@ class FreqtradeBot(object):
logger.info("No currency pair in whitelist, but checking to sell open trades.") logger.info("No currency pair in whitelist, but checking to sell open trades.")
return False return False
buycount = 0
# running get_signal on historical data fetched # running get_signal on historical data fetched
for _pair in whitelist: for _pair in whitelist:
if self.strategy.is_pair_locked(_pair):
logger.info(f"Pair {_pair} is currently locked.")
continue
(buy, sell) = self.strategy.get_signal( (buy, sell) = self.strategy.get_signal(
_pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval)) _pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
if buy and not sell: if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']:
stake_amount = self._get_trade_stake_amount(_pair) stake_amount = self._get_trade_stake_amount(_pair)
if not stake_amount: if not stake_amount:
return False continue
logger.info(f"Buy signal found: about create a new trade with stake_amount: " logger.info(f"Buy signal found: about create a new trade with stake_amount: "
f"{stake_amount} ...") f"{stake_amount} ...")
@@ -294,12 +306,13 @@ class FreqtradeBot(object):
if (bidstrat_check_depth_of_market.get('enabled', False)) and\ if (bidstrat_check_depth_of_market.get('enabled', False)) and\
(bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0):
if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market): if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market):
return self.execute_buy(_pair, stake_amount) buycount += self.execute_buy(_pair, stake_amount)
else: else:
return False continue
return self.execute_buy(_pair, stake_amount)
return False buycount += self.execute_buy(_pair, stake_amount)
return buycount > 0
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
""" """
@@ -423,21 +436,17 @@ class FreqtradeBot(object):
return True return True
def process_maybe_execute_buy(self) -> bool: def process_maybe_execute_buy(self) -> None:
""" """
Tries to execute a buy trade in a safe way Tries to execute a buy trade in a safe way
:return: True if executed :return: True if executed
""" """
try: try:
# Create entity and execute trade # Create entity and execute trade
if self.create_trade(): if not self.create_trades():
return True logger.info('Found no buy signals for whitelisted currencies. Trying again...')
logger.info('Found no buy signals for whitelisted currencies. Trying again..')
return False
except DependencyException as exception: except DependencyException as exception:
logger.warning('Unable to create trade: %s', exception) logger.warning('Unable to create trade: %s', exception)
return False
def process_maybe_execute_sell(self, trade: Trade) -> bool: def process_maybe_execute_sell(self, trade: Trade) -> bool:
""" """
@@ -478,8 +487,11 @@ class FreqtradeBot(object):
return order_amount return order_amount
# use fee from order-dict if possible # use fee from order-dict if possible
if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}): if ('fee' in order and order['fee'] is not None and
if trade.pair.startswith(order['fee']['currency']): (order['fee'].keys() >= {'currency', 'cost'})):
if (order['fee']['currency'] is not None and
order['fee']['cost'] is not None and
trade.pair.startswith(order['fee']['currency'])):
new_amount = order_amount - order['fee']['cost'] new_amount = order_amount - order['fee']['cost']
logger.info("Applying fee on amount for %s (from %s to %s) from Order", logger.info("Applying fee on amount for %s (from %s to %s) from Order",
trade, order['amount'], new_amount) trade, order['amount'], new_amount)
@@ -496,9 +508,12 @@ class FreqtradeBot(object):
fee_abs = 0 fee_abs = 0
for exectrade in trades: for exectrade in trades:
amount += exectrade['amount'] amount += exectrade['amount']
if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}): if ("fee" in exectrade and exectrade['fee'] is not None and
(exectrade['fee'].keys() >= {'currency', 'cost'})):
# only applies if fee is in quote currency! # only applies if fee is in quote currency!
if trade.pair.startswith(exectrade['fee']['currency']): if (exectrade['fee']['currency'] is not None and
exectrade['fee']['cost'] is not None and
trade.pair.startswith(exectrade['fee']['currency'])):
fee_abs += exectrade['fee']['cost'] fee_abs += exectrade['fee']['cost']
if amount != order_amount: if amount != order_amount:
@@ -518,7 +533,11 @@ class FreqtradeBot(object):
if trade.open_order_id: if trade.open_order_id:
# Update trade with order values # Update trade with order values
logger.info('Found open order for %s', trade) logger.info('Found open order for %s', trade)
order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) try:
order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair)
except InvalidOrderException as exception:
logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception)
return
# Try update amount (binance-fix) # Try update amount (binance-fix)
try: try:
new_amount = self.get_real_amount(trade, order) new_amount = self.get_real_amount(trade, order)
@@ -586,13 +605,13 @@ class FreqtradeBot(object):
logger.info(' order book asks top %s: %0.8f', i, order_book_rate) logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
sell_rate = order_book_rate sell_rate = order_book_rate
if self.check_sell(trade, sell_rate, buy, sell): if self._check_and_execute_sell(trade, sell_rate, buy, sell):
return True return True
else: else:
logger.debug('checking sell') logger.debug('checking sell')
sell_rate = self.get_sell_rate(trade.pair, True) sell_rate = self.get_sell_rate(trade.pair, True)
if self.check_sell(trade, sell_rate, buy, sell): if self._check_and_execute_sell(trade, sell_rate, buy, sell):
return True return True
logger.debug('Found no sell signal for %s.', trade) logger.debug('Found no sell signal for %s.', trade)
@@ -643,6 +662,7 @@ class FreqtradeBot(object):
return False return False
except DependencyException as exception: except DependencyException as exception:
trade.stoploss_order_id = None
logger.warning('Unable to place a stoploss order on exchange: %s', exception) logger.warning('Unable to place a stoploss order on exchange: %s', exception)
# If stoploss order is canceled for some reason we add it # If stoploss order is canceled for some reason we add it
@@ -655,6 +675,7 @@ class FreqtradeBot(object):
trade.stoploss_order_id = str(stoploss_order_id) trade.stoploss_order_id = str(stoploss_order_id)
return False return False
except DependencyException as exception: except DependencyException as exception:
trade.stoploss_order_id = None
logger.warning('Stoploss order was cancelled, ' logger.warning('Stoploss order was cancelled, '
'but unable to recreate one: %s', exception) 'but unable to recreate one: %s', exception)
@@ -662,7 +683,10 @@ class FreqtradeBot(object):
if stoploss_order and stoploss_order['status'] == 'closed': if stoploss_order and stoploss_order['status'] == 'closed':
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
trade.update(stoploss_order) trade.update(stoploss_order)
self.notify_sell(trade) # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['ticker_interval']))
self._notify_sell(trade)
return True return True
# Finally we check if stoploss on exchange should be moved up because of trailing. # Finally we check if stoploss on exchange should be moved up because of trailing.
@@ -704,16 +728,19 @@ class FreqtradeBot(object):
)['id'] )['id']
trade.stoploss_order_id = str(stoploss_order_id) trade.stoploss_order_id = str(stoploss_order_id)
except DependencyException: except DependencyException:
logger.exception(f"Could create trailing stoploss order " trade.stoploss_order_id = None
logger.exception(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.") f"for pair {trade.pair}.")
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
if self.edge: buy: bool, sell: bool) -> bool:
stoploss = self.edge.stoploss(trade.pair) """
should_sell = self.strategy.should_sell( Check and execute sell
trade, sell_rate, datetime.utcnow(), buy, sell, force_stoploss=stoploss) """
else: should_sell = self.strategy.should_sell(
should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell) trade, sell_rate, datetime.utcnow(), buy, sell,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
)
if should_sell.sell_flag: if should_sell.sell_flag:
self.execute_sell(trade, sell_rate, should_sell.sell_type) self.execute_sell(trade, sell_rate, should_sell.sell_type)
@@ -741,7 +768,7 @@ class FreqtradeBot(object):
if not trade.open_order_id: if not trade.open_order_id:
continue continue
order = self.exchange.get_order(trade.open_order_id, trade.pair) order = self.exchange.get_order(trade.open_order_id, trade.pair)
except (RequestException, DependencyException): except (RequestException, DependencyException, InvalidOrderException):
logger.info( logger.info(
'Cannot query order for %s due to %s', 'Cannot query order for %s due to %s',
trade, trade,
@@ -857,19 +884,26 @@ class FreqtradeBot(object):
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
# Execute sell and update trade record # Execute sell and update trade record
order_id = self.exchange.sell(pair=str(trade.pair), order = self.exchange.sell(pair=str(trade.pair),
ordertype=self.strategy.order_types[sell_type], ordertype=self.strategy.order_types[sell_type],
amount=trade.amount, rate=limit, amount=trade.amount, rate=limit,
time_in_force=self.strategy.order_time_in_force['sell'] time_in_force=self.strategy.order_time_in_force['sell']
)['id'] )
trade.open_order_id = order_id trade.open_order_id = order['id']
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.sell_reason = sell_reason.value trade.sell_reason = sell_reason.value
# In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed':
trade.update(order)
Trade.session.flush() Trade.session.flush()
self.notify_sell(trade)
def notify_sell(self, trade: Trade): # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
self._notify_sell(trade)
def _notify_sell(self, trade: Trade):
""" """
Sends rpc notification when a sell occured. Sends rpc notification when a sell occured.
""" """

50
freqtrade/loggers.py Normal file
View File

@@ -0,0 +1,50 @@
import logging
import sys
from logging.handlers import RotatingFileHandler
from typing import Any, Dict, List
logger = logging.getLogger(__name__)
def _set_loggers(verbosity: int = 0) -> None:
"""
Set the logging level for third party libraries
:return: None
"""
logging.getLogger('requests').setLevel(
logging.INFO if verbosity <= 1 else logging.DEBUG
)
logging.getLogger("urllib3").setLevel(
logging.INFO if verbosity <= 1 else logging.DEBUG
)
logging.getLogger('ccxt.base.exchange').setLevel(
logging.INFO if verbosity <= 2 else logging.DEBUG
)
logging.getLogger('telegram').setLevel(logging.INFO)
def setup_logging(config: Dict[str, Any]) -> None:
"""
Process -v/--verbose, --logfile options
"""
# Log level
verbosity = config['verbosity']
# Log to stdout, not stderr
log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)]
if config.get('logfile'):
log_handlers.append(RotatingFileHandler(config['logfile'],
maxBytes=1024 * 1024, # 1Mb
backupCount=10))
logging.basicConfig(
level=logging.INFO if verbosity < 1 else logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=log_handlers
)
_set_loggers(verbosity)
logger.info('Verbosity set to %s', verbosity)

View File

@@ -15,8 +15,7 @@ from argparse import Namespace
from typing import Any, List from typing import Any, List
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.configuration import set_loggers
from freqtrade.worker import Worker from freqtrade.worker import Worker
@@ -32,8 +31,6 @@ def main(sysargv: List[str] = None) -> None:
return_code: Any = 1 return_code: Any = 1
worker = None worker = None
try: try:
set_loggers()
arguments = Arguments( arguments = Arguments(
sysargv, sysargv,
'Free, open source crypto trading bot' 'Free, open source crypto trading bot'

View File

@@ -5,13 +5,12 @@ import gzip
import logging import logging
import re import re
from datetime import datetime from datetime import datetime
from typing import Dict from pathlib import Path
from typing.io import IO
import numpy as np import numpy as np
from pandas import DataFrame
import rapidjson import rapidjson
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -41,25 +40,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
return dates.dt.to_pydatetime() return dates.dt.to_pydatetime()
def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray: def file_dump_json(filename: Path, data, is_zip=False) -> None:
"""
Return dates from Dataframe
:param dfs: Dict with format pair: pair_data
:return: List of dates
"""
alldates = {}
for pair, pair_data in dfs.items():
dates = datesarray_to_datetimearray(pair_data['date'])
for date in dates:
alldates[date] = 1
lst = []
for date, _ in alldates.items():
lst.append(date)
arr = np.array(lst)
return np.sort(arr, axis=0)
def file_dump_json(filename, data, is_zip=False) -> None:
""" """
Dump JSON data into a file Dump JSON data into a file
:param filename: file to create :param filename: file to create
@@ -69,8 +50,8 @@ def file_dump_json(filename, data, is_zip=False) -> None:
logger.info(f'dumping json to "{filename}"') logger.info(f'dumping json to "{filename}"')
if is_zip: if is_zip:
if not filename.endswith('.gz'): if filename.suffix != '.gz':
filename = filename + '.gz' filename = filename.with_suffix('.gz')
with gzip.open(filename, 'w') as fp: with gzip.open(filename, 'w') as fp:
rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE)
else: else:
@@ -80,7 +61,7 @@ def file_dump_json(filename, data, is_zip=False) -> None:
logger.debug(f'done json to "{filename}"') logger.debug(f'done json to "{filename}"')
def json_load(datafile): def json_load(datafile: IO):
""" """
load data with rapidjson load data with rapidjson
Use this to have a consistent experience, Use this to have a consistent experience,

View File

@@ -64,14 +64,14 @@ def start_hyperopt(args: Namespace) -> None:
:return: None :return: None
""" """
# Import here to avoid loading hyperopt module when it's not used # Import here to avoid loading hyperopt module when it's not used
from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE from freqtrade.optimize.hyperopt import Hyperopt
# Initialize configuration # Initialize configuration
config = setup_configuration(args, RunMode.HYPEROPT) config = setup_configuration(args, RunMode.HYPEROPT)
logger.info('Starting freqtrade in Hyperopt mode') logger.info('Starting freqtrade in Hyperopt mode')
lock = FileLock(HYPEROPT_LOCKFILE) lock = FileLock(Hyperopt.get_lock_filename(config))
try: try:
with lock.acquire(timeout=1): with lock.acquire(timeout=1):

View File

@@ -10,9 +10,9 @@ from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional from typing import Any, Dict, List, NamedTuple, Optional
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate
from freqtrade.arguments import Arguments from freqtrade import OperationalException
from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
@@ -21,6 +21,7 @@ from freqtrade.persistence import Trade
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.strategy.interface import IStrategy, SellType
from tabulate import tabulate
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -88,6 +89,9 @@ class Backtesting(object):
Load strategy into backtesting Load strategy into backtesting
""" """
self.strategy = strategy self.strategy = strategy
if "ticker_interval" not in self.config:
raise OperationalException("Ticker-interval needs to be set in either configuration "
"or as cli argument `--ticker-interval 5m`")
self.ticker_interval = self.config.get('ticker_interval') self.ticker_interval = self.config.get('ticker_interval')
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval) self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
@@ -186,7 +190,7 @@ class Backtesting(object):
return tabulate(tabular_data, headers=headers, # type: ignore return tabulate(tabular_data, headers=headers, # type: ignore
floatfmt=floatfmt, tablefmt="pipe") floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: str, results: DataFrame, def _store_backtest_result(self, recordfilename: Path, results: DataFrame,
strategyname: Optional[str] = None) -> None: strategyname: Optional[str] = None) -> None:
records = [(t.pair, t.profit_percent, t.open_time.timestamp(), records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
@@ -197,10 +201,10 @@ class Backtesting(object):
if records: if records:
if strategyname: if strategyname:
# Inject strategyname to filename # Inject strategyname to filename
recname = Path(recordfilename) recordfilename = Path.joinpath(
recordfilename = str(Path.joinpath( recordfilename.parent,
recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix)) f'{recordfilename.stem}-{strategyname}').with_suffix(recordfilename.suffix)
logger.info('Dumping backtest results to %s', recordfilename) logger.info(f'Dumping backtest results to {recordfilename}')
file_dump_json(recordfilename, records) file_dump_json(recordfilename, records)
def _get_ticker_list(self, processed) -> Dict[str, DataFrame]: def _get_ticker_list(self, processed) -> Dict[str, DataFrame]:
@@ -252,22 +256,20 @@ class Backtesting(object):
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy,
sell_row.sell, low=sell_row.low, high=sell_row.high) sell_row.sell, low=sell_row.low, high=sell_row.high)
if sell.sell_flag: if sell.sell_flag:
trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60)
# Special handling if high or low hit STOP_LOSS or ROI # Special handling if high or low hit STOP_LOSS or ROI
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
# Set close_rate to stoploss # Set close_rate to stoploss
closerate = trade.stop_loss closerate = trade.stop_loss
elif sell.sell_type == (SellType.ROI): elif sell.sell_type == (SellType.ROI):
# get next entry in min_roi > to trade duration roi = self.strategy.min_roi_reached_entry(trade_dur)
# Interface.py skips on trade_duration <= duration if roi is not None:
roi_entry = max(list(filter(lambda x: trade_dur >= x, # - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
self.strategy.minimal_roi.keys()))) closerate = - (trade.open_rate * roi + trade.open_rate *
roi = self.strategy.minimal_roi[roi_entry] (1 + trade.fee_open)) / (trade.fee_close - 1)
else:
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1) # This should not be reached...
closerate = - (trade.open_rate * roi + trade.open_rate * closerate = sell_row.open
(1 + trade.fee_open)) / (trade.fee_close - 1)
else: else:
closerate = sell_row.open closerate = sell_row.open
@@ -321,6 +323,9 @@ class Backtesting(object):
position_stacking: do we allow position stacking? (default: False) position_stacking: do we allow position stacking? (default: False)
:return: DataFrame :return: DataFrame
""" """
# Arguments are long and noisy, so this is commented out.
# Uncomment if you need to debug the backtest() method.
# logger.debug(f"Start backtest, args: {args}")
processed = args['processed'] processed = args['processed']
stake_amount = args['stake_amount'] stake_amount = args['stake_amount']
max_open_trades = args.get('max_open_trades', 0) max_open_trades = args.get('max_open_trades', 0)
@@ -372,7 +377,9 @@ class Backtesting(object):
continue continue
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]:], # since indexes has been incremented before, we need to go one step back to
# also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(pair, row, ticker[pair][indexes[pair]-1:],
trade_count_lock, stake_amount, trade_count_lock, stake_amount,
max_open_trades) max_open_trades)
@@ -397,7 +404,7 @@ class Backtesting(object):
logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount']) logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
timerange = Arguments.parse_timerange(None if self.config.get( timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) 'timerange') is None else str(self.config.get('timerange')))
data = history.load_data( data = history.load_data(
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
@@ -406,7 +413,6 @@ class Backtesting(object):
refresh_pairs=self.config.get('refresh_pairs', False), refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.exchange, exchange=self.exchange,
timerange=timerange, timerange=timerange,
live=self.config.get('live', False)
) )
if not data: if not data:
@@ -451,7 +457,7 @@ class Backtesting(object):
for strategy, results in all_results.items(): for strategy, results in all_results.items():
if self.config.get('export', False): if self.config.get('export', False):
self._store_backtest_result(self.config['exportfilename'], results, self._store_backtest_result(Path(self.config['exportfilename']), results,
strategy if len(self.strategylist) > 1 else None) strategy if len(self.strategylist) > 1 else None)
print(f"Result for strategy {strategy}") print(f"Result for strategy {strategy}")

View File

@@ -1,52 +1,61 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from functools import reduce
from typing import Any, Callable, Dict, List
import talib.abstract as ta import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
from typing import Dict, Any, Callable, List from skopt.space import Categorical, Dimension, Integer
from functools import reduce
from skopt.space import Categorical, Dimension, Integer, Real
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
class_name = 'DefaultHyperOpts'
class DefaultHyperOpts(IHyperOpt): class DefaultHyperOpts(IHyperOpt):
""" """
Default hyperopt provided by freqtrade bot. Default hyperopt provided by the Freqtrade bot.
You can override it with your own hyperopt You can override it with your own Hyperopt
""" """
@staticmethod @staticmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Add several indicators needed for buy and sell strategies defined below.
"""
# ADX
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
# MACD
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'] dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdsignal'] = macd['macdsignal']
# MFI
dataframe['mfi'] = ta.MFI(dataframe) dataframe['mfi'] = ta.MFI(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe) dataframe['rsi'] = ta.RSI(dataframe)
# Stochastic Fast
stoch_fast = ta.STOCHF(dataframe) stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastd'] = stoch_fast['fastd']
# Minus-DI
dataframe['minus_di'] = ta.MINUS_DI(dataframe) dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Bollinger bands # Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower'] dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_upperband'] = bollinger['upper'] dataframe['bb_upperband'] = bollinger['upper']
# SAR
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
return dataframe return dataframe
@staticmethod @staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable: def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
Define the buy strategy parameters to be used by hyperopt Define the buy strategy parameters to be used by Hyperopt.
""" """
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Buy strategy Hyperopt will build and use Buy strategy Hyperopt will build and use.
""" """
conditions = [] conditions = []
# GUARDS AND TRENDS # GUARDS AND TRENDS
if 'mfi-enabled' in params and params['mfi-enabled']: if 'mfi-enabled' in params and params['mfi-enabled']:
conditions.append(dataframe['mfi'] < params['mfi-value']) conditions.append(dataframe['mfi'] < params['mfi-value'])
@@ -82,7 +91,7 @@ class DefaultHyperOpts(IHyperOpt):
@staticmethod @staticmethod
def indicator_space() -> List[Dimension]: def indicator_space() -> List[Dimension]:
""" """
Define your Hyperopt space for searching strategy parameters Define your Hyperopt space for searching buy strategy parameters.
""" """
return [ return [
Integer(10, 25, name='mfi-value'), Integer(10, 25, name='mfi-value'),
@@ -99,14 +108,14 @@ class DefaultHyperOpts(IHyperOpt):
@staticmethod @staticmethod
def sell_strategy_generator(params: Dict[str, Any]) -> Callable: def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
Define the sell strategy parameters to be used by hyperopt Define the sell strategy parameters to be used by Hyperopt.
""" """
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Sell strategy Hyperopt will build and use Sell strategy Hyperopt will build and use.
""" """
# print(params)
conditions = [] conditions = []
# GUARDS AND TRENDS # GUARDS AND TRENDS
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
conditions.append(dataframe['mfi'] > params['sell-mfi-value']) conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
@@ -142,7 +151,7 @@ class DefaultHyperOpts(IHyperOpt):
@staticmethod @staticmethod
def sell_indicator_space() -> List[Dimension]: def sell_indicator_space() -> List[Dimension]:
""" """
Define your Hyperopt space for searching sell strategy parameters Define your Hyperopt space for searching sell strategy parameters.
""" """
return [ return [
Integer(75, 100, name='sell-mfi-value'), Integer(75, 100, name='sell-mfi-value'),
@@ -158,47 +167,11 @@ class DefaultHyperOpts(IHyperOpt):
'sell-sar_reversal'], name='sell-trigger') 'sell-sar_reversal'], name='sell-trigger')
] ]
@staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]:
"""
Generate the ROI table that will be used by Hyperopt
"""
roi_table = {}
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
return roi_table
@staticmethod
def stoploss_space() -> List[Dimension]:
"""
Stoploss Value to search
"""
return [
Real(-0.5, -0.02, name='stoploss'),
]
@staticmethod
def roi_space() -> List[Dimension]:
"""
Values to search for each ROI steps
"""
return [
Integer(10, 120, name='roi_t1'),
Integer(10, 60, name='roi_t2'),
Integer(10, 40, name='roi_t3'),
Real(0.01, 0.04, name='roi_p1'),
Real(0.01, 0.07, name='roi_p2'),
Real(0.01, 0.20, name='roi_p3'),
]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators. Should be a copy of from strategy Based on TA indicators. Should be a copy of same method from strategy.
must align to populate_indicators in this file Must align to populate_indicators in this file.
Only used when --spaces does not include buy Only used when --spaces does not include buy space.
""" """
dataframe.loc[ dataframe.loc[
( (
@@ -213,9 +186,9 @@ class DefaultHyperOpts(IHyperOpt):
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators. Should be a copy of from strategy Based on TA indicators. Should be a copy of same method from strategy.
must align to populate_indicators in this file Must align to populate_indicators in this file.
Only used when --spaces does not include sell Only used when --spaces does not include sell space.
""" """
dataframe.loc[ dataframe.loc[
( (
@@ -225,4 +198,5 @@ class DefaultHyperOpts(IHyperOpt):
(dataframe['fastd'] > 54) (dataframe['fastd'] > 54)
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe

View File

@@ -0,0 +1,52 @@
"""
DefaultHyperOptLoss
This module defines the default HyperoptLoss class which is being used for
Hyperoptimization.
"""
from math import exp
from pandas import DataFrame
from freqtrade.optimize.hyperopt import IHyperOptLoss
# Set TARGET_TRADES to suit your number concurrent trades so its realistic
# to the number of days
TARGET_TRADES = 600
# This is assumed to be expected avg profit * expected trade count.
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
# expected max profit = 3.85
# Check that the reported Σ% values do not exceed this!
# Note, this is ratio. 3.85 stated above means 385Σ%.
EXPECTED_MAX_PROFIT = 3.0
# Max average trade duration in minutes.
# If eval ends with higher value, we consider it a failed eval.
MAX_ACCEPTED_TRADE_DURATION = 300
class DefaultHyperOptLoss(IHyperOptLoss):
"""
Defines the default loss function for hyperopt
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results
This is the Default algorithm
Weights are distributed as follows:
* 0.4 to trade duration
* 0.25: Avoiding trade loss
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
"""
total_profit = results.profit_percent.sum()
trade_duration = results.trade_duration.mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
result = trade_loss + profit_loss + duration_loss
return result

View File

@@ -9,7 +9,7 @@ from tabulate import tabulate
from freqtrade import constants from freqtrade import constants
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.arguments import Arguments from freqtrade.configuration import TimeRange
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
@@ -41,7 +41,7 @@ class EdgeCli(object):
self.edge = Edge(config, self.exchange, self.strategy) self.edge = Edge(config, self.exchange, self.strategy)
self.edge._refresh_pairs = self.config.get('refresh_pairs', False) self.edge._refresh_pairs = self.config.get('refresh_pairs', False)
self.timerange = Arguments.parse_timerange(None if self.config.get( self.timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) 'timerange') is None else str(self.config.get('timerange')))
self.edge._timerange = self.timerange self.edge._timerange = self.timerange

View File

@@ -5,23 +5,29 @@ This module contains the hyperopt logic
""" """
import logging import logging
import os
import sys import sys
from math import exp
from collections import OrderedDict
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
from pprint import pprint from pprint import pprint
from typing import Any, Dict, List from typing import Any, Dict, List, Optional
import rapidjson
from colorama import init as colorama_init
from colorama import Fore, Style
from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count from joblib import Parallel, delayed, dump, load, wrap_non_picklable_objects, cpu_count
from pandas import DataFrame from pandas import DataFrame
from skopt import Optimizer from skopt import Optimizer
from skopt.space import Dimension from skopt.space import Dimension
from freqtrade.arguments import Arguments from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_data, get_timeframe from freqtrade.data.history import load_data, get_timeframe
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # Import IHyperOptLoss to allow users import from this file
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -29,12 +35,9 @@ logger = logging.getLogger(__name__)
INITIAL_POINTS = 30 INITIAL_POINTS = 30
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl')
TRIALSDATA_PICKLE = os.path.join('user_data', 'hyperopt_results.pickle')
HYPEROPT_LOCKFILE = os.path.join('user_data', 'hyperopt.lock')
class Hyperopt(Backtesting): class Hyperopt:
""" """
Hyperopt class, this class contains all the logic to run a hyperopt simulation Hyperopt class, this class contains all the logic to run a hyperopt simulation
@@ -43,30 +46,66 @@ class Hyperopt(Backtesting):
hyperopt.start() hyperopt.start()
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
super().__init__(config) self.config = config
self.backtesting = Backtesting(self.config)
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
# set TARGET_TRADES to suit your number concurrent trades so its realistic self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
# to the number of days self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
self.target_trades = 600
self.total_tries = config.get('epochs', 0) self.trials_file = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_results.pickle')
self.tickerdata_pickle = (self.config['user_data_dir'] /
'hyperopt_results' / 'hyperopt_tickerdata.pkl')
self.total_epochs = config.get('epochs', 0)
self.current_best_loss = 100 self.current_best_loss = 100
# max average trade duration in minutes if not self.config.get('hyperopt_continue'):
# if eval ends with higher value, we consider it a failed eval self.clean_hyperopt()
self.max_accepted_trade_duration = 300 else:
logger.info("Continuing on previous hyperopt results.")
# This is assumed to be expected avg profit * expected trade count.
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
# self.expected_max_profit = 3.85
# Check that the reported Σ% values do not exceed this!
# Note, this is ratio. 3.85 stated above means 385Σ%.
self.expected_max_profit = 3.0
# Previous evaluations # Previous evaluations
self.trials_file = TRIALSDATA_PICKLE
self.trials: List = [] self.trials: List = []
# Populate functions here (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.backtesting.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.backtesting.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True):
self.max_open_trades = self.config['max_open_trades']
else:
logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
self.max_open_trades = 0
self.position_stacking = self.config.get('position_stacking', False),
if self.has_space('sell'):
# Make sure experimental is enabled
if 'experimental' not in self.config:
self.config['experimental'] = {}
self.config['experimental']['use_sell_signal'] = True
@staticmethod
def get_lock_filename(config) -> str:
return str(config['user_data_dir'] / 'hyperopt.lock')
def clean_hyperopt(self):
"""
Remove hyperopt pickle files to restart hyperopt.
"""
for f in [self.tickerdata_pickle, self.trials_file]:
p = Path(f)
if p.is_file():
logger.info(f"Removing `{p}`.")
p.unlink()
def get_args(self, params): def get_args(self, params):
dimensions = self.hyperopt_space() dimensions = self.hyperopt_space()
# Ensure the number of dimensions match # Ensure the number of dimensions match
@@ -94,7 +133,7 @@ class Hyperopt(Backtesting):
""" """
logger.info('Reading Trials from \'%s\'', self.trials_file) logger.info('Reading Trials from \'%s\'', self.trials_file)
trials = load(self.trials_file) trials = load(self.trials_file)
os.remove(self.trials_file) self.trials_file.unlink()
return trials return trials
def log_trials_result(self) -> None: def log_trials_result(self) -> None:
@@ -103,108 +142,144 @@ class Hyperopt(Backtesting):
""" """
results = sorted(self.trials, key=itemgetter('loss')) results = sorted(self.trials, key=itemgetter('loss'))
best_result = results[0] best_result = results[0]
logger.info( params = best_result['params']
'Best result:\n%s\nwith values:\n', log_str = self.format_results_logstring(best_result)
best_result['result'] print(f"\nBest result:\n\n{log_str}\n")
)
pprint(best_result['params'], indent=4) if self.config.get('print_json'):
if 'roi_t1' in best_result['params']: result_dict: Dict = {}
logger.info('ROI table:') if self.has_space('buy') or self.has_space('sell'):
pprint(self.custom_hyperopt.generate_roi_table(best_result['params']), indent=4) result_dict['params'] = {}
if self.has_space('buy'):
result_dict['params'].update({p.name: params.get(p.name)
for p in self.hyperopt_space('buy')})
if self.has_space('sell'):
result_dict['params'].update({p.name: params.get(p.name)
for p in self.hyperopt_space('sell')})
if self.has_space('roi'):
# Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys...
# OrderedDict is used to keep the numeric order of the items
# in the dict.
result_dict['minimal_roi'] = OrderedDict(
(str(k), v) for k, v in self.custom_hyperopt.generate_roi_table(params).items()
)
if self.has_space('stoploss'):
result_dict['stoploss'] = params.get('stoploss')
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
if self.has_space('buy'):
print('Buy hyperspace params:')
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('buy')},
indent=4)
if self.has_space('sell'):
print('Sell hyperspace params:')
pprint({p.name: params.get(p.name) for p in self.hyperopt_space('sell')},
indent=4)
if self.has_space('roi'):
print("ROI table:")
pprint(self.custom_hyperopt.generate_roi_table(params), indent=4)
if self.has_space('stoploss'):
print(f"Stoploss: {params.get('stoploss')}")
def log_results(self, results) -> None: def log_results(self, results) -> None:
""" """
Log results if it is better than any previous evaluation Log results if it is better than any previous evaluation
""" """
print_all = self.config.get('print_all', False) print_all = self.config.get('print_all', False)
if print_all or results['loss'] < self.current_best_loss: is_best_loss = results['loss'] < self.current_best_loss
# Output human-friendly index here (starting from 1) if print_all or is_best_loss:
current = results['current_tries'] + 1 if is_best_loss:
total = results['total_tries'] self.current_best_loss = results['loss']
res = results['result'] log_str = self.format_results_logstring(results)
loss = results['loss'] # Colorize output
self.current_best_loss = results['loss'] if self.config.get('print_colorized', False):
log_msg = f'{current:5d}/{total}: {res} Objective: {loss:.5f}' if results['total_profit'] > 0:
log_msg = f'*{log_msg}' if results['initial_point'] else f' {log_msg}' log_str = Fore.GREEN + log_str
if print_all and is_best_loss:
log_str = Style.BRIGHT + log_str
if print_all: if print_all:
print(log_msg) print(log_str)
else: else:
print('\n' + log_msg) print('\n' + log_str)
else: else:
print('.', end='') print('.', end='')
sys.stdout.flush() sys.stdout.flush()
def calculate_loss(self, total_profit: float, trade_count: int, trade_duration: float) -> float: def format_results_logstring(self, results) -> str:
""" # Output human-friendly index here (starting from 1)
Objective function, returns smaller number for more optimal results current = results['current_epoch'] + 1
""" total = self.total_epochs
trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8) res = results['results_explanation']
profit_loss = max(0, 1 - total_profit / self.expected_max_profit) loss = results['loss']
duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1) log_str = f'{current:5d}/{total}: {res} Objective: {loss:.5f}'
result = trade_loss + profit_loss + duration_loss log_str = f'*{log_str}' if results['is_initial_point'] else f' {log_str}'
return result return log_str
def has_space(self, space: str) -> bool: def has_space(self, space: str) -> bool:
""" """
Tell if a space value is contained in the configuration Tell if a space value is contained in the configuration
""" """
if space in self.config['spaces'] or 'all' in self.config['spaces']: return any(s in self.config['spaces'] for s in [space, 'all'])
return True
return False
def hyperopt_space(self) -> List[Dimension]: def hyperopt_space(self, space: Optional[str] = None) -> List[Dimension]:
""" """
Return the space to use during Hyperopt 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] = [] spaces: List[Dimension] = []
if self.has_space('buy'): if space == 'buy' or (space is None and self.has_space('buy')):
logger.debug("Hyperopt has 'buy' space")
spaces += self.custom_hyperopt.indicator_space() spaces += self.custom_hyperopt.indicator_space()
if self.has_space('sell'): if space == 'sell' or (space is None and self.has_space('sell')):
logger.debug("Hyperopt has 'sell' space")
spaces += self.custom_hyperopt.sell_indicator_space() spaces += self.custom_hyperopt.sell_indicator_space()
# Make sure experimental is enabled if space == 'roi' or (space is None and self.has_space('roi')):
if 'experimental' not in self.config: logger.debug("Hyperopt has 'roi' space")
self.config['experimental'] = {}
self.config['experimental']['use_sell_signal'] = True
if self.has_space('roi'):
spaces += self.custom_hyperopt.roi_space() spaces += self.custom_hyperopt.roi_space()
if self.has_space('stoploss'): if space == 'stoploss' or (space is None and self.has_space('stoploss')):
logger.debug("Hyperopt has 'stoploss' space")
spaces += self.custom_hyperopt.stoploss_space() spaces += self.custom_hyperopt.stoploss_space()
return spaces return spaces
def generate_optimizer(self, _params: Dict) -> Dict: def generate_optimizer(self, _params: Dict) -> Dict:
"""
Used Optimize function. Called once per epoch to optimize whatever is configured.
Keep this function as optimized as possible!
"""
params = self.get_args(_params) params = self.get_args(_params)
if self.has_space('roi'): if self.has_space('roi'):
self.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params)
if self.has_space('buy'): if self.has_space('buy'):
self.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) self.backtesting.advise_buy = self.custom_hyperopt.buy_strategy_generator(params)
elif hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore
if self.has_space('sell'): if self.has_space('sell'):
self.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) self.backtesting.advise_sell = self.custom_hyperopt.sell_strategy_generator(params)
elif hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.strategy.stoploss = params['stoploss'] self.backtesting.strategy.stoploss = params['stoploss']
processed = load(self.tickerdata_pickle)
processed = load(TICKERDATA_PICKLE)
min_date, max_date = get_timeframe(processed) min_date, max_date = get_timeframe(processed)
results = self.backtest(
results = self.backtesting.backtest(
{ {
'stake_amount': self.config['stake_amount'], 'stake_amount': self.config['stake_amount'],
'processed': processed, 'processed': processed,
'position_stacking': self.config.get('position_stacking', True), 'max_open_trades': self.max_open_trades,
'position_stacking': self.position_stacking,
'start_date': min_date, 'start_date': min_date,
'end_date': max_date, 'end_date': max_date,
} }
) )
result_explanation = self.format_results(results) results_explanation = self.format_results(results)
total_profit = results.profit_percent.sum()
trade_count = len(results.index) trade_count = len(results.index)
trade_duration = results.trade_duration.mean() total_profit = results.profit_abs.sum()
# If this evaluation contains too short amount of trades to be # If this evaluation contains too short amount of trades to be
# interesting -- consider it as 'bad' (assigned max. loss value) # interesting -- consider it as 'bad' (assigned max. loss value)
@@ -214,20 +289,23 @@ class Hyperopt(Backtesting):
return { return {
'loss': MAX_LOSS, 'loss': MAX_LOSS,
'params': params, 'params': params,
'result': result_explanation, 'results_explanation': results_explanation,
'total_profit': total_profit,
} }
loss = self.calculate_loss(total_profit, trade_count, trade_duration) loss = self.calculate_loss(results=results, trade_count=trade_count,
min_date=min_date.datetime, max_date=max_date.datetime)
return { return {
'loss': loss, 'loss': loss,
'params': params, 'params': params,
'result': result_explanation, 'results_explanation': results_explanation,
'total_profit': total_profit,
} }
def format_results(self, results: DataFrame) -> str: def format_results(self, results: DataFrame) -> str:
""" """
Return the format result in a string Return the formatted results explanation in a string
""" """
trades = len(results.index) trades = len(results.index)
avg_profit = results.profit_percent.mean() * 100.0 avg_profit = results.profit_percent.mean() * 100.0
@@ -256,7 +334,7 @@ class Hyperopt(Backtesting):
def load_previous_results(self): def load_previous_results(self):
""" read trials file if we have one """ """ read trials file if we have one """
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0: if self.trials_file.is_file() and self.trials_file.stat().st_size > 0:
self.trials = self.read_trials() self.trials = self.read_trials()
logger.info( logger.info(
'Loaded %d previous evaluations from disk.', 'Loaded %d previous evaluations from disk.',
@@ -264,14 +342,14 @@ class Hyperopt(Backtesting):
) )
def start(self) -> None: def start(self) -> None:
timerange = Arguments.parse_timerange(None if self.config.get( timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) 'timerange') is None else str(self.config.get('timerange')))
data = load_data( data = load_data(
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, datadir=Path(self.config['datadir']) if self.config.get('datadir') else None,
pairs=self.config['exchange']['pair_whitelist'], pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.ticker_interval, ticker_interval=self.backtesting.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False), refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.exchange, exchange=self.backtesting.exchange,
timerange=timerange timerange=timerange
) )
@@ -288,16 +366,15 @@ class Hyperopt(Backtesting):
(max_date - min_date).days (max_date - min_date).days
) )
if self.has_space('buy') or self.has_space('sell'): self.backtesting.strategy.advise_indicators = \
self.strategy.advise_indicators = \ self.custom_hyperopt.populate_indicators # type: ignore
self.custom_hyperopt.populate_indicators # type: ignore
preprocessed = self.strategy.tickerdata_to_dataframe(data) preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
dump(preprocessed, TICKERDATA_PICKLE) dump(preprocessed, self.tickerdata_pickle)
# We don't need exchange instance anymore while running hyperopt # We don't need exchange instance anymore while running hyperopt
self.exchange = None # type: ignore self.backtesting.exchange = None # type: ignore
self.load_previous_results() self.load_previous_results()
@@ -307,29 +384,27 @@ class Hyperopt(Backtesting):
logger.info(f'Number of parallel jobs set as: {config_jobs}') logger.info(f'Number of parallel jobs set as: {config_jobs}')
opt = self.get_optimizer(config_jobs) opt = self.get_optimizer(config_jobs)
if self.config.get('print_colorized', False):
colorama_init(autoreset=True)
try: try:
with Parallel(n_jobs=config_jobs) as parallel: with Parallel(n_jobs=config_jobs) as parallel:
jobs = parallel._effective_n_jobs() jobs = parallel._effective_n_jobs()
logger.info(f'Effective number of parallel workers used: {jobs}') logger.info(f'Effective number of parallel workers used: {jobs}')
EVALS = max(self.total_tries // jobs, 1) EVALS = max(self.total_epochs // jobs, 1)
for i in range(EVALS): for i in range(EVALS):
asked = opt.ask(n_points=jobs) asked = opt.ask(n_points=jobs)
f_val = self.run_optimizer_parallel(parallel, asked) f_val = self.run_optimizer_parallel(parallel, asked)
opt.tell(asked, [i['loss'] for i in f_val]) opt.tell(asked, [v['loss'] for v in f_val])
self.trials += f_val
for j in range(jobs): for j in range(jobs):
current = i * jobs + j current = i * jobs + j
self.log_results({ val = f_val[j]
'loss': f_val[j]['loss'], val['current_epoch'] = current
'current_tries': current, val['is_initial_point'] = current < INITIAL_POINTS
'initial_point': current < INITIAL_POINTS, self.log_results(val)
'total_tries': self.total_tries, self.trials.append(val)
'result': f_val[j]['result'], logger.debug(f"Optimizer epoch evaluated: {val}")
})
logger.debug(f"Optimizer params: {f_val[j]['params']}")
for j in range(jobs):
logger.debug(f"Optimizer state: Xi: {opt.Xi[-j-1]}, yi: {opt.yi[-j-1]}")
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print('User interrupted..')

View File

@@ -7,7 +7,7 @@ from abc import ABC, abstractmethod
from typing import Dict, Any, Callable, List from typing import Dict, Any, Callable, List
from pandas import DataFrame from pandas import DataFrame
from skopt.space import Dimension from skopt.space import Dimension, Integer, Real
class IHyperOpt(ABC): class IHyperOpt(ABC):
@@ -26,56 +26,80 @@ class IHyperOpt(ABC):
@abstractmethod @abstractmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Populate indicators that will be used in the Buy and Sell strategy Populate indicators that will be used in the Buy and Sell strategy.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe().
:return: a Dataframe with all mandatory indicators for the strategies :return: A Dataframe with all mandatory indicators for the strategies.
""" """
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable: def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
Create a buy strategy generator Create a buy strategy generator.
""" """
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def sell_strategy_generator(params: Dict[str, Any]) -> Callable: def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
Create a sell strategy generator Create a sell strategy generator.
""" """
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def indicator_space() -> List[Dimension]: def indicator_space() -> List[Dimension]:
""" """
Create an indicator space Create an indicator space.
""" """
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def sell_indicator_space() -> List[Dimension]: def sell_indicator_space() -> List[Dimension]:
""" """
Create a sell indicator space Create a sell indicator space.
""" """
@staticmethod @staticmethod
@abstractmethod
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> Dict[int, float]:
""" """
Create an roi table Create a ROI table.
Generates the ROI table that will be used by Hyperopt.
You may override it in your custom Hyperopt class.
""" """
roi_table = {}
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
return roi_table
@staticmethod @staticmethod
@abstractmethod
def stoploss_space() -> List[Dimension]: def stoploss_space() -> List[Dimension]:
""" """
Create a stoploss space Create a stoploss space.
Defines range of stoploss values to search.
You may override it in your custom Hyperopt class.
""" """
return [
Real(-0.5, -0.02, name='stoploss'),
]
@staticmethod @staticmethod
@abstractmethod
def roi_space() -> List[Dimension]: def roi_space() -> List[Dimension]:
""" """
Create a roi space Create a ROI space.
Defines values to search for each ROI steps.
You may override it in your custom Hyperopt class.
""" """
return [
Integer(10, 120, name='roi_t1'),
Integer(10, 60, name='roi_t2'),
Integer(10, 40, name='roi_t3'),
Real(0.01, 0.04, name='roi_p1'),
Real(0.01, 0.07, name='roi_p2'),
Real(0.01, 0.20, name='roi_p3'),
]

View File

@@ -0,0 +1,25 @@
"""
IHyperOptLoss interface
This module defines the interface for the loss-function for hyperopts
"""
from abc import ABC, abstractmethod
from datetime import datetime
from pandas import DataFrame
class IHyperOptLoss(ABC):
"""
Interface for freqtrade hyperopts Loss functions.
Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.)
"""
ticker_interval: str
@staticmethod
@abstractmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime, *args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results
"""

View File

@@ -0,0 +1,38 @@
"""
OnlyProfitHyperOptLoss
This module defines the alternative HyperOptLoss class which can be used for
Hyperoptimization.
"""
from pandas import DataFrame
from freqtrade.optimize.hyperopt import IHyperOptLoss
# This is assumed to be expected avg profit * expected trade count.
# For example, for 0.35% avg per trade (or 0.0035 as ratio) and 1100 trades,
# expected max profit = 3.85
#
# Note, this is ratio. 3.85 stated above means 385Σ%, 3.0 means 300Σ%.
#
# In this implementation it's only used in calculation of the resulting value
# of the objective function as a normalization coefficient and does not
# represent any limit for profits as in the Freqtrade legacy default loss function.
EXPECTED_MAX_PROFIT = 3.0
class OnlyProfitHyperOptLoss(IHyperOptLoss):
"""
Defines the loss function for hyperopt.
This implementation takes only profit into account.
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results.
"""
total_profit = results.profit_percent.sum()
return 1 - total_profit / EXPECTED_MAX_PROFIT

View File

@@ -0,0 +1,45 @@
"""
SharpeHyperOptLoss
This module defines the alternative HyperOptLoss class which can be used for
Hyperoptimization.
"""
from datetime import datetime
from pandas import DataFrame
import numpy as np
from freqtrade.optimize.hyperopt import IHyperOptLoss
class SharpeHyperOptLoss(IHyperOptLoss):
"""
Defines the loss function for hyperopt.
This implementation uses the Sharpe Ratio calculation.
"""
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for more optimal results.
Uses Sharpe Ratio calculation.
"""
total_profit = results.profit_percent
days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade
total_profit = total_profit - 0.0005
expected_yearly_return = total_profit.sum() / days_period
if (np.std(total_profit) != 0.):
sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365)
else:
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
sharp_ratio = -20.
# print(expected_yearly_return, np.std(total_profit), sharp_ratio)
return -sharp_ratio

View File

@@ -55,7 +55,6 @@ class VolumePairList(IPairList):
# Generate dynamic whitelist # Generate dynamic whitelist
self._whitelist = self._gen_pair_whitelist( self._whitelist = self._gen_pair_whitelist(
self._config['stake_currency'], self._sort_key)[:self._number_pairs] self._config['stake_currency'], self._sort_key)[:self._number_pairs]
logger.info(f"Searching pairs: {self._whitelist}")
@cached(TTLCache(maxsize=1, ttl=1800)) @cached(TTLCache(maxsize=1, ttl=1800))
def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]:
@@ -92,4 +91,6 @@ class VolumePairList(IPairList):
valid_tickers.remove(t) valid_tickers.remove(t)
pairs = [s['symbol'] for s in valid_tickers] pairs = [s['symbol'] for s in valid_tickers]
logger.info(f"Searching pairs: {self._whitelist}")
return pairs return pairs

View File

@@ -1,22 +1,67 @@
import logging import logging
from typing import List from pathlib import Path
from typing import Dict, List, Optional
import pandas as pd import pandas as pd
from pathlib import Path
from freqtrade.configuration import TimeRange
from freqtrade.data import history
from freqtrade.data.btanalysis import (combine_tickers_with_mean,
create_cum_profit, load_trades)
from freqtrade.exchange import Exchange
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: try:
from plotly import tools from plotly.subplots import make_subplots
from plotly.offline import plot from plotly.offline import plot
import plotly.graph_objs as go import plotly.graph_objects as go
except ImportError: except ImportError:
logger.exception("Module plotly not found \n Please install using `pip install plotly`") logger.exception("Module plotly not found \n Please install using `pip install plotly`")
exit(1) exit(1)
def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.make_subplots: def init_plotscript(config):
"""
Initialize objects needed for plotting
:return: Dict with tickers, trades, pairs and strategy
"""
exchange: Optional[Exchange] = None
# Exchange is only needed when downloading data!
if config.get("refresh_pairs", False):
exchange = ExchangeResolver(config.get('exchange', {}).get('name'),
config).exchange
strategy = StrategyResolver(config).strategy
if "pairs" in config:
pairs = config["pairs"]
else:
pairs = config["exchange"]["pair_whitelist"]
# Set timerange to use
timerange = TimeRange.parse_timerange(config.get("timerange"))
tickers = history.load_data(
datadir=Path(str(config.get("datadir"))),
pairs=pairs,
ticker_interval=config['ticker_interval'],
refresh_pairs=config.get('refresh_pairs', False),
timerange=timerange,
exchange=exchange,
)
trades = load_trades(config)
return {"tickers": tickers,
"trades": trades,
"pairs": pairs,
"strategy": strategy,
}
def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots:
""" """
Generator all the indicator selected by the user for a specific row Generator all the indicator selected by the user for a specific row
:param fig: Plot figure to append to :param fig: Plot figure to append to
@@ -33,7 +78,7 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m
mode='lines', mode='lines',
name=indicator name=indicator
) )
fig.append_trace(scattergl, row, 1) fig.add_trace(scattergl, row, 1)
else: else:
logger.info( logger.info(
'Indicator "%s" ignored. Reason: This indicator is not found ' 'Indicator "%s" ignored. Reason: This indicator is not found '
@@ -44,9 +89,29 @@ def generate_row(fig, row, indicators: List[str], data: pd.DataFrame) -> tools.m
return fig return fig
def plot_trades(fig, trades: pd.DataFrame): def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots:
""" """
Plot trades to "fig" Add profit-plot
:param fig: Plot figure to append to
:param row: row number for this plot
:param data: candlestick DataFrame
:param column: Column to use for plot
:param name: Name to use
:return: fig with added profit plot
"""
profit = go.Scattergl(
x=data.index,
y=data[column],
name=name,
)
fig.add_trace(profit, row, 1)
return fig
def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
"""
Add trades to "fig"
""" """
# Trades can be empty # Trades can be empty
if trades is not None and len(trades) > 0: if trades is not None and len(trades) > 0:
@@ -79,20 +144,16 @@ def plot_trades(fig, trades: pd.DataFrame):
color='red' color='red'
) )
) )
fig.append_trace(trade_buys, 1, 1) fig.add_trace(trade_buys, 1, 1)
fig.append_trace(trade_sells, 1, 1) fig.add_trace(trade_sells, 1, 1)
else: else:
logger.warning("No trades found.") logger.warning("No trades found.")
return fig return fig
def generate_graph( def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None,
pair: str, indicators1: List[str] = [],
data: pd.DataFrame, indicators2: List[str] = [],) -> go.Figure:
trades: pd.DataFrame = None,
indicators1: List[str] = [],
indicators2: List[str] = [],
) -> go.Figure:
""" """
Generate the graph from the data generated by Backtesting or from DB Generate the graph from the data generated by Backtesting or from DB
Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators
@@ -105,7 +166,7 @@ def generate_graph(
""" """
# Define the graph # Define the graph
fig = tools.make_subplots( fig = make_subplots(
rows=3, rows=3,
cols=1, cols=1,
shared_xaxes=True, shared_xaxes=True,
@@ -127,7 +188,7 @@ def generate_graph(
close=data.close, close=data.close,
name='Price' name='Price'
) )
fig.append_trace(candles, 1, 1) fig.add_trace(candles, 1, 1)
if 'buy' in data.columns: if 'buy' in data.columns:
df_buy = data[data['buy'] == 1] df_buy = data[data['buy'] == 1]
@@ -144,7 +205,7 @@ def generate_graph(
color='green', color='green',
) )
) )
fig.append_trace(buys, 1, 1) fig.add_trace(buys, 1, 1)
else: else:
logger.warning("No buy-signals found.") logger.warning("No buy-signals found.")
@@ -163,7 +224,7 @@ def generate_graph(
color='red', color='red',
) )
) )
fig.append_trace(sells, 1, 1) fig.add_trace(sells, 1, 1)
else: else:
logger.warning("No sell-signals found.") logger.warning("No sell-signals found.")
@@ -182,11 +243,11 @@ def generate_graph(
fillcolor="rgba(0,176,246,0.2)", fillcolor="rgba(0,176,246,0.2)",
line={'color': 'rgba(255,255,255,0)'}, line={'color': 'rgba(255,255,255,0)'},
) )
fig.append_trace(bb_lower, 1, 1) fig.add_trace(bb_lower, 1, 1)
fig.append_trace(bb_upper, 1, 1) fig.add_trace(bb_upper, 1, 1)
# Add indicators to main plot # Add indicators to main plot
fig = generate_row(fig=fig, row=1, indicators=indicators1, data=data) fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data)
fig = plot_trades(fig, trades) fig = plot_trades(fig, trades)
@@ -196,15 +257,57 @@ def generate_graph(
y=data['volume'], y=data['volume'],
name='Volume' name='Volume'
) )
fig.append_trace(volume, 2, 1) fig.add_trace(volume, 2, 1)
# Add indicators to seperate row # Add indicators to seperate row
fig = generate_row(fig=fig, row=3, indicators=indicators2, data=data) fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data)
return fig return fig
def generate_plot_file(fig, pair, ticker_interval) -> None: def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
trades: pd.DataFrame) -> go.Figure:
# Combine close-values for all pairs, rename columns to "pair"
df_comb = combine_tickers_with_mean(tickers, "close")
# Add combined cumulative profit
df_comb = create_cum_profit(df_comb, trades, 'cum_profit')
# Plot the pairs average close prices, and total profit growth
avgclose = go.Scattergl(
x=df_comb.index,
y=df_comb['mean'],
name='Avg close price',
)
fig = make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1])
fig['layout'].update(title="Profit plot")
fig.add_trace(avgclose, 1, 1)
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
for pair in pairs:
profit_col = f'cum_profit_{pair}'
df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col)
fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}")
return fig
def generate_plot_filename(pair, ticker_interval) -> str:
"""
Generate filenames per pair/ticker_interval to be used for storing plots
"""
pair_name = pair.replace("/", "_")
file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html'
logger.info('Generate plot file for %s', pair)
return file_name
def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False) -> None:
""" """
Generate a plot html file from pre populated fig plotly object Generate a plot html file from pre populated fig plotly object
:param fig: Plotly Figure to plot :param fig: Plotly Figure to plot
@@ -212,12 +315,9 @@ def generate_plot_file(fig, pair, ticker_interval) -> None:
:param ticker_interval: Used as part of the filename :param ticker_interval: Used as part of the filename
:return: None :return: None
""" """
logger.info('Generate plot file for %s', pair) directory.mkdir(parents=True, exist_ok=True)
pair_name = pair.replace("/", "_") _filename = directory.joinpath(filename)
file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' plot(fig, filename=str(_filename),
auto_open=auto_open)
Path("user_data/plots").mkdir(parents=True, exist_ok=True) logger.info(f"Stored plot as {_filename}")
plot(fig, filename=str(Path('user_data/plots').joinpath(file_name)),
auto_open=False)

View File

@@ -28,6 +28,7 @@ class ExchangeResolver(IResolver):
except ImportError: except ImportError:
logger.info( logger.info(
f"No {exchange_name} specific subclass found. Using the generic class instead.") f"No {exchange_name} specific subclass found. Using the generic class instead.")
if not hasattr(self, "exchange"):
self.exchange = Exchange(config) self.exchange = Exchange(config)
def _load_exchange( def _load_exchange(
@@ -44,13 +45,13 @@ class ExchangeResolver(IResolver):
exchange = ex_class(kwargs['config']) exchange = ex_class(kwargs['config'])
if exchange: if exchange:
logger.info("Using resolved exchange %s", exchange_name) logger.info(f"Using resolved exchange '{exchange_name}'...")
return exchange return exchange
except AttributeError: except AttributeError:
# Pass and raise ImportError instead # Pass and raise ImportError instead
pass pass
raise ImportError( raise ImportError(
"Impossible to load Exchange '{}'. This class does not exist" f"Impossible to load Exchange '{exchange_name}'. This class does not exist "
" or contains Python code errors".format(exchange_name) "or contains Python code errors."
) )

View File

@@ -7,8 +7,10 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Optional, Dict from typing import Optional, Dict
from freqtrade.constants import DEFAULT_HYPEROPT from freqtrade import OperationalException
from freqtrade.constants import DEFAULT_HYPEROPT, DEFAULT_HYPEROPT_LOSS
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -21,16 +23,16 @@ class HyperOptResolver(IResolver):
__slots__ = ['hyperopt'] __slots__ = ['hyperopt']
def __init__(self, config: Optional[Dict] = None) -> None: def __init__(self, config: Dict) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary or None :param config: configuration dictionary
""" """
config = config or {}
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt # Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT hyperopt_name = config.get('hyperopt') or DEFAULT_HYPEROPT
self.hyperopt = self._load_hyperopt(hyperopt_name, extra_dir=config.get('hyperopt_path')) self.hyperopt = self._load_hyperopt(hyperopt_name, config,
extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt # Assign ticker_interval to be used in hyperopt
self.hyperopt.__class__.ticker_interval = str(config['ticker_interval']) self.hyperopt.__class__.ticker_interval = str(config['ticker_interval'])
@@ -43,35 +45,88 @@ class HyperOptResolver(IResolver):
"Using populate_sell_trend from DefaultStrategy.") "Using populate_sell_trend from DefaultStrategy.")
def _load_hyperopt( def _load_hyperopt(
self, hyperopt_name: str, extra_dir: Optional[str] = None) -> IHyperOpt: self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt:
""" """
Search and loads the specified hyperopt. Search and loads the specified hyperopt.
:param hyperopt_name: name of the module to import :param hyperopt_name: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given hyperopt :param extra_dir: additional directory to search for the given hyperopt
:return: HyperOpt instance or None :return: HyperOpt instance or None
""" """
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = [ abs_paths = [
current_path.parent.parent.joinpath('user_data/hyperopts'), config['user_data_dir'].joinpath('hyperopts'),
current_path, current_path,
] ]
if extra_dir: if extra_dir:
# Add extra hyperopt directory on top of search paths # Add extra hyperopt directory on top of search paths
abs_paths.insert(0, Path(extra_dir)) abs_paths.insert(0, Path(extra_dir).resolve())
for _path in abs_paths: hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt,
try: object_name=hyperopt_name)
hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, if hyperopt:
object_name=hyperopt_name) return hyperopt
if hyperopt: raise OperationalException(
logger.info("Using resolved hyperopt %s from '%s'", hyperopt_name, _path) f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist "
return hyperopt "or contains Python code errors."
except FileNotFoundError: )
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
raise ImportError( class HyperOptLossResolver(IResolver):
"Impossible to load Hyperopt '{}'. This class does not exist" """
" or contains Python code errors".format(hyperopt_name) This class contains all the logic to load custom hyperopt loss class
"""
__slots__ = ['hyperoptloss']
def __init__(self, config: Dict = None) -> None:
"""
Load the custom class from config parameter
:param config: configuration dictionary or None
"""
config = config or {}
# Verify the hyperopt is in the configuration, otherwise fallback to the default hyperopt
hyperopt_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
self.hyperoptloss = self._load_hyperoptloss(
hyperopt_name, config, extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt
self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'):
raise OperationalException(
f"Found hyperopt {hyperopt_name} does not implement `hyperopt_loss_function`.")
def _load_hyperoptloss(
self, hyper_loss_name: str, config: Dict,
extra_dir: Optional[str] = None) -> IHyperOptLoss:
"""
Search and loads the specified hyperopt loss class.
:param hyper_loss_name: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given hyperopt
:return: HyperOptLoss instance or None
"""
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = [
config['user_data_dir'].joinpath('hyperopts'),
current_path,
]
if extra_dir:
# Add extra hyperopt directory on top of search paths
abs_paths.insert(0, Path(extra_dir).resolve())
hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss,
object_name=hyper_loss_name)
if hyperoptloss:
return hyperoptloss
raise OperationalException(
f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist "
"or contains Python code errors."
) )

View File

@@ -7,7 +7,7 @@ import importlib.util
import inspect import inspect
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, Type, Any from typing import Any, List, Optional, Tuple, Type, Union
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -29,7 +29,8 @@ class IResolver(object):
""" """
# Generate spec based on absolute path # Generate spec based on absolute path
spec = importlib.util.spec_from_file_location('unknown', str(module_path)) # Pass object_name as first argument to have logging print a reasonable name.
spec = importlib.util.spec_from_file_location(object_name, str(module_path))
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
try: try:
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
@@ -45,7 +46,7 @@ class IResolver(object):
@staticmethod @staticmethod
def _search_object(directory: Path, object_type, object_name: str, def _search_object(directory: Path, object_type, object_name: str,
kwargs: dict = {}) -> Optional[Any]: kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]:
""" """
Search for the objectname in the given directory Search for the objectname in the given directory
:param directory: relative or absolute directory path :param directory: relative or absolute directory path
@@ -57,9 +58,33 @@ class IResolver(object):
if not str(entry).endswith('.py'): if not str(entry).endswith('.py'):
logger.debug('Ignoring %s', entry) logger.debug('Ignoring %s', entry)
continue continue
module_path = entry.resolve()
obj = IResolver._get_valid_object( obj = IResolver._get_valid_object(
object_type, Path.resolve(directory.joinpath(entry)), object_name object_type, module_path, object_name
) )
if obj: if obj:
return obj(**kwargs) return (obj(**kwargs), module_path)
return (None, None)
@staticmethod
def _load_object(paths: List[Path], object_type, object_name: str,
kwargs: dict = {}) -> Optional[Any]:
"""
Try to load object from path list.
"""
for _path in paths:
try:
(module, module_path) = IResolver._search_object(directory=_path,
object_type=object_type,
object_name=object_name,
kwargs=kwargs)
if module:
logger.info(
f"Using resolved {object_type.__name__.lower()[1:]} {object_name} "
f"from '{module_path}'...")
return module
except FileNotFoundError:
logger.warning('Path "%s" does not exist.', _path.resolve())
return None return None

View File

@@ -6,6 +6,7 @@ This module load custom hyperopts
import logging import logging
from pathlib import Path from pathlib import Path
from freqtrade import OperationalException
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
@@ -24,36 +25,30 @@ class PairListResolver(IResolver):
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary or None :param config: configuration dictionary or None
""" """
self.pairlist = self._load_pairlist(pairlist_name, kwargs={'freqtrade': freqtrade, self.pairlist = self._load_pairlist(pairlist_name, config, kwargs={'freqtrade': freqtrade,
'config': config}) 'config': config})
def _load_pairlist( def _load_pairlist(
self, pairlist_name: str, kwargs: dict) -> IPairList: self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList:
""" """
Search and loads the specified pairlist. Search and loads the specified pairlist.
:param pairlist_name: name of the module to import :param pairlist_name: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given pairlist :param extra_dir: additional directory to search for the given pairlist
:return: PairList instance or None :return: PairList instance or None
""" """
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
abs_paths = [ abs_paths = [
current_path.parent.parent.joinpath('user_data/pairlist'), config['user_data_dir'].joinpath('pairlist'),
current_path, current_path,
] ]
for _path in abs_paths: pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
try: object_name=pairlist_name, kwargs=kwargs)
pairlist = self._search_object(directory=_path, object_type=IPairList, if pairlist:
object_name=pairlist_name, return pairlist
kwargs=kwargs) raise OperationalException(
if pairlist: f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist "
logger.info("Using resolved pairlist %s from '%s'", pairlist_name, _path) "or contains Python code errors."
return pairlist
except FileNotFoundError:
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
raise ImportError(
"Impossible to load Pairlist '{}'. This class does not exist"
" or contains Python code errors".format(pairlist_name)
) )

View File

@@ -11,7 +11,7 @@ from inspect import getfullargspec
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
from freqtrade import constants from freqtrade import constants, OperationalException
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
from freqtrade.strategy import import_strategy from freqtrade.strategy import import_strategy
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
@@ -123,7 +123,7 @@ class StrategyResolver(IResolver):
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
abs_paths = [ abs_paths = [
Path.cwd().joinpath('user_data/strategies'), config['user_data_dir'].joinpath('strategies'),
current_path, current_path,
] ]
@@ -132,7 +132,7 @@ class StrategyResolver(IResolver):
abs_paths.insert(0, Path(extra_dir).resolve()) abs_paths.insert(0, Path(extra_dir).resolve())
if ":" in strategy_name: if ":" in strategy_name:
logger.info("loading base64 endocded strategy") logger.info("loading base64 encoded strategy")
strat = strategy_name.split(":") strat = strategy_name.split(":")
if len(strat) == 2: if len(strat) == 2:
@@ -147,25 +147,21 @@ class StrategyResolver(IResolver):
# register temp path with the bot # register temp path with the bot
abs_paths.insert(0, temp.resolve()) abs_paths.insert(0, temp.resolve())
for _path in abs_paths: strategy = self._load_object(paths=abs_paths, object_type=IStrategy,
try: object_name=strategy_name, kwargs={'config': config})
strategy = self._search_object(directory=_path, object_type=IStrategy, if strategy:
object_name=strategy_name, kwargs={'config': config}) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
if strategy: strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
logger.info("Using resolved strategy %s from '%s'", strategy_name, _path) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
strategy._populate_fun_len = len(
getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
try:
return import_strategy(strategy, config=config)
except TypeError as e:
logger.warning(
f"Impossible to load strategy '{strategy}' from {_path}. Error: {e}")
except FileNotFoundError:
logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd()))
raise ImportError( try:
f"Impossible to load Strategy '{strategy_name}'. This class does not exist" return import_strategy(strategy, config=config)
" or contains Python code errors" except TypeError as e:
logger.warning(
f"Impossible to load strategy '{strategy_name}'. "
f"Error: {e}")
raise OperationalException(
f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
"or contains Python code errors."
) )

View File

@@ -10,7 +10,7 @@ from typing import Dict, Any, List, Optional
import arrow import arrow
import sqlalchemy as sql import sqlalchemy as sql
from numpy import mean, nan_to_num, NAN from numpy import mean, NAN
from pandas import DataFrame from pandas import DataFrame
from freqtrade import TemporaryError, DependencyException from freqtrade import TemporaryError, DependencyException
@@ -195,9 +195,9 @@ class RPC(object):
trades = Trade.query.order_by(Trade.id).all() trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = [] profit_all_coin = []
profit_all_percent = [] profit_all_perc = []
profit_closed_coin = [] profit_closed_coin = []
profit_closed_percent = [] profit_closed_perc = []
durations = [] durations = []
for trade in trades: for trade in trades:
@@ -211,7 +211,7 @@ class RPC(object):
if not trade.is_open: if not trade.is_open:
profit_percent = trade.calc_profit_percent() profit_percent = trade.calc_profit_percent()
profit_closed_coin.append(trade.calc_profit()) profit_closed_coin.append(trade.calc_profit())
profit_closed_percent.append(profit_percent) profit_closed_perc.append(profit_percent)
else: else:
# Get current rate # Get current rate
try: try:
@@ -223,7 +223,7 @@ class RPC(object):
profit_all_coin.append( profit_all_coin.append(
trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)) trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))
) )
profit_all_percent.append(profit_percent) profit_all_perc.append(profit_percent)
best_pair = Trade.session.query( best_pair = Trade.session.query(
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum') Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
@@ -238,7 +238,8 @@ class RPC(object):
# Prepare data to display # Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8) profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2) profit_closed_percent = (round(mean(profit_closed_perc) * 100, 2) if profit_closed_perc
else 0.0)
profit_closed_fiat = self._fiat_converter.convert_amount( profit_closed_fiat = self._fiat_converter.convert_amount(
profit_closed_coin_sum, profit_closed_coin_sum,
stake_currency, stake_currency,
@@ -246,7 +247,7 @@ class RPC(object):
) if self._fiat_converter else 0 ) if self._fiat_converter else 0
profit_all_coin_sum = round(sum(profit_all_coin), 8) profit_all_coin_sum = round(sum(profit_all_coin), 8)
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2) profit_all_percent = round(mean(profit_all_perc) * 100, 2) if profit_all_perc else 0.0
profit_all_fiat = self._fiat_converter.convert_amount( profit_all_fiat = self._fiat_converter.convert_amount(
profit_all_coin_sum, profit_all_coin_sum,
stake_currency, stake_currency,
@@ -281,10 +282,11 @@ class RPC(object):
rate = 1.0 rate = 1.0
else: else:
try: try:
if coin in('USDT', 'USD', 'EUR'): pair = self._freqtrade.exchange.get_valid_pair_combination(coin, "BTC")
rate = 1.0 / self._freqtrade.get_sell_rate('BTC/' + coin, False) if pair.startswith("BTC"):
rate = 1.0 / self._freqtrade.get_sell_rate(pair, False)
else: else:
rate = self._freqtrade.get_sell_rate(coin + '/BTC', False) rate = self._freqtrade.get_sell_rate(pair, False)
except (TemporaryError, DependencyException): except (TemporaryError, DependencyException):
logger.warning(f" Could not get rate for pair {coin}.") logger.warning(f" Could not get rate for pair {coin}.")
continue continue
@@ -298,7 +300,10 @@ class RPC(object):
'est_btc': est_btc, 'est_btc': est_btc,
}) })
if total == 0.0: if total == 0.0:
raise RPCException('all balances are zero') if self._freqtrade.config.get('dry_run', False):
raise RPCException('Running in Dry Run, balances are not available.')
else:
raise RPCException('All balances are zero.')
symbol = fiat_display_currency symbol = fiat_display_currency
value = self._fiat_converter.convert_amount(total, 'BTC', value = self._fiat_converter.convert_amount(total, 'BTC',

View File

@@ -217,7 +217,8 @@ class Telegram(RPC):
"*Open Order:* `{open_order}`" if r['open_order'] else "" "*Open Order:* `{open_order}`" if r['open_order'] else ""
] ]
messages.append("\n".join(filter(None, lines)).format(**r)) # Filter empty lines using list-comprehension
messages.append("\n".join([l for l in lines if l]).format(**r))
for msg in messages: for msg in messages:
self._send_msg(msg, bot=bot) self._send_msg(msg, bot=bot)

View File

@@ -4,9 +4,9 @@ This module defines the interface to apply for strategies
""" """
import logging import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime from datetime import datetime, timezone
from enum import Enum from enum import Enum
from typing import Dict, List, NamedTuple, Tuple from typing import Dict, List, NamedTuple, Optional, Tuple
import warnings import warnings
import arrow import arrow
@@ -107,6 +107,7 @@ class IStrategy(ABC):
self.config = config self.config = config
# Dict to determine if analysis is necessary # Dict to determine if analysis is necessary
self._last_candle_seen_per_pair: Dict[str, datetime] = {} self._last_candle_seen_per_pair: Dict[str, datetime] = {}
self._pair_locked_until: Dict[str, datetime] = {}
@abstractmethod @abstractmethod
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -154,10 +155,45 @@ class IStrategy(ABC):
""" """
return self.__class__.__name__ return self.__class__.__name__
def lock_pair(self, pair: str, until: datetime) -> None:
"""
Locks pair until a given timestamp happens.
Locked pairs are not analyzed, and are prevented from opening new trades.
:param pair: Pair to lock
:param until: datetime in UTC until the pair should be blocked from opening new trades.
Needs to be timezone aware `datetime.now(timezone.utc)`
"""
self._pair_locked_until[pair] = until
def is_pair_locked(self, pair: str) -> bool:
"""
Checks if a pair is currently locked
"""
if pair not in self._pair_locked_until:
return False
return self._pair_locked_until[pair] >= datetime.now(timezone.utc)
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Parses the given ticker history and returns a populated DataFrame Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it add several TA indicators and buy signal to it
:param dataframe: Dataframe containing ticker data
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
:return: DataFrame with ticker data and indicator data
"""
logger.debug("TA Analysis Launched")
dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.advise_buy(dataframe, metadata)
dataframe = self.advise_sell(dataframe, metadata)
return dataframe
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it
WARNING: Used internally only, may skip analysis if `process_only_new_candles` is set.
:param dataframe: Dataframe containing ticker data
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
:return: DataFrame with ticker data and indicator data :return: DataFrame with ticker data and indicator data
""" """
@@ -168,10 +204,7 @@ class IStrategy(ABC):
if (not self.process_only_new_candles or if (not self.process_only_new_candles or
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']): self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
# Defs that only make change on new candle data. # Defs that only make change on new candle data.
logger.debug("TA Analysis Launched") dataframe = self.analyze_ticker(dataframe, metadata)
dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.advise_buy(dataframe, metadata)
dataframe = self.advise_sell(dataframe, metadata)
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
else: else:
logger.debug("Skipping TA Analysis for already analyzed candle") logger.debug("Skipping TA Analysis for already analyzed candle")
@@ -198,7 +231,7 @@ class IStrategy(ABC):
return False, False return False, False
try: try:
dataframe = self.analyze_ticker(dataframe, {'pair': pair}) dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair})
except ValueError as error: except ValueError as error:
logger.warning( logger.warning(
'Unable to analyze ticker for pair %s: %s', 'Unable to analyze ticker for pair %s: %s',
@@ -246,8 +279,8 @@ class IStrategy(ABC):
sell: bool, low: float = None, high: float = None, sell: bool, low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple: force_stoploss: float = 0) -> SellCheckTuple:
""" """
This function evaluate if on the condition required to trigger a sell has been reached This function evaluates if one of the conditions required to trigger a sell
if the threshold is reached and updates the trade record. has been reached, which can either be a stop-loss, ROI or sell-signal.
:param low: Only used during backtesting to simulate stoploss :param low: Only used during backtesting to simulate stoploss
:param high: Only used during backtesting, to simulate ROI :param high: Only used during backtesting, to simulate ROI
:param force_stoploss: Externally provided stoploss :param force_stoploss: Externally provided stoploss
@@ -347,23 +380,32 @@ class IStrategy(ABC):
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
def min_roi_reached_entry(self, trade_dur: int) -> Optional[float]:
"""
Based on trade duration defines the ROI entry that may have been reached.
:param trade_dur: trade duration in minutes
:return: minimal ROI entry value or None if none proper ROI entry was found.
"""
# Get highest entry in ROI dict where key <= trade-duration
roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys()))
if not roi_list:
return None
roi_entry = max(roi_list)
return self.minimal_roi[roi_entry]
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
""" """
Based an earlier trade and current price and ROI configuration, decides whether bot should Based on trade duration, current price and ROI configuration, decides whether bot should
sell. Requires current_profit to be in percent!! sell. Requires current_profit to be in percent!!
:return: True if bot should sell at current rate :return: True if bot should sell at current rate
""" """
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60 trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60)
roi = self.min_roi_reached_entry(trade_dur)
# Get highest entry in ROI dict where key >= trade-duration if roi is None:
roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys()))) return False
threshold = self.minimal_roi[roi_entry] else:
if current_profit > threshold: return current_profit > roi
return True
return False
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
""" """

View File

@@ -0,0 +1,133 @@
{
/* Single-line C-style comment */
"max_open_trades": 3,
/*
* Multi-line C-style comment
*/
"stake_currency": "BTC",
"stake_amount": 0.05,
"fiat_display_currency": "USD", // C++-style comment
"amount_reserve_percent" : 0.05, // And more, tabs before this comment
"dry_run": false,
"ticker_interval": "5m",
"trailing_stop": false,
"trailing_stop_positive": 0.005,
"trailing_stop_positive_offset": 0.0051,
"trailing_only_offset_is_reached": false,
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.10,
"unfilledtimeout": {
"buy": 10,
"sell": 30, // Trailing comma should also be accepted now
},
"bid_strategy": {
"use_order_book": false,
"ask_last_balance": 0.0,
"order_book_top": 1,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"ask_strategy":{
"use_order_book": false,
"order_book_min": 1,
"order_book_max": 9
},
"order_types": {
"buy": "limit",
"sell": "limit",
"stoploss": "market",
"stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60
},
"order_time_in_force": {
"buy": "gtc",
"sell": "gtc"
},
"pairlist": {
"method": "VolumePairList",
"config": {
"number_assets": 20,
"sort_key": "quoteVolume",
"precision_filter": false
}
},
"exchange": {
"name": "bittrex",
"sandbox": false,
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"password": "",
"ccxt_config": {"enableRateLimit": true},
"ccxt_async_config": {
"enableRateLimit": false,
"rateLimit": 500,
"aiohttp_trust_env": false
},
"pair_whitelist": [
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"XLM/BTC",
"NXT/BTC",
"POWR/BTC",
"ADA/BTC",
"XMR/BTC"
],
"pair_blacklist": [
"DOGE/BTC"
],
"outdated_offset": 5,
"markets_refresh_interval": 60
},
"edge": {
"enabled": false,
"process_throttle_secs": 3600,
"calculate_since_number_of_days": 7,
"capital_available_percentage": 0.5,
"allowed_risk": 0.01,
"stoploss_range_min": -0.01,
"stoploss_range_max": -0.1,
"stoploss_range_step": -0.01,
"minimum_winrate": 0.60,
"minimum_expectancy": 0.20,
"min_trade_number": 10,
"max_trade_duration_minute": 1440,
"remove_pumps": false
},
"experimental": {
"use_sell_signal": false,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
},
"telegram": {
// We can now comment out some settings
// "enabled": true,
"enabled": false,
"token": "your_telegram_token",
"chat_id": "your_telegram_chat_id"
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"username": "freqtrader",
"password": "SuperSecurePassword"
},
"db_url": "sqlite:///tradesv3.sqlite",
"initial_state": "running",
"forcebuy_enable": false,
"internals": {
"process_throttle_secs": 5
},
"strategy": "DefaultStrategy",
"strategy_path": "user_data/strategies/"
}

View File

@@ -6,15 +6,15 @@ from copy import deepcopy
from datetime import datetime from datetime import datetime
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
from typing import List
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import arrow import arrow
import pytest import pytest
import numpy as np
from telegram import Chat, Message, Update from telegram import Chat, Message, Update
from freqtrade import constants, persistence from freqtrade import constants, persistence
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.edge import Edge, PairInfo from freqtrade.edge import Edge, PairInfo
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@@ -22,27 +22,39 @@ from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.worker import Worker from freqtrade.worker import Worker
logging.getLogger('').setLevel(logging.INFO) logging.getLogger('').setLevel(logging.INFO)
# Do not mask numpy errors as warnings that no one read, raise the exсeption
np.seterr(all='raise')
def log_has(line, logs): def log_has(line, logs):
# caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar') # caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar')
# and we want to match line against foobar in the tuple # and we want to match line against foobar in the tuple
return reduce(lambda a, b: a or b, return reduce(lambda a, b: a or b,
filter(lambda x: x[2] == line, logs), filter(lambda x: x[2] == line, logs.record_tuples),
False) False)
def log_has_re(line, logs): def log_has_re(line, logs):
return reduce(lambda a, b: a or b, return reduce(lambda a, b: a or b,
filter(lambda x: re.match(line, x[2]), logs), filter(lambda x: re.match(line, x[2]), logs.record_tuples),
False) False)
def get_args(args) -> List[str]: def get_args(args):
return Arguments(args, '').get_parsed_arg() return Arguments(args, '').get_parsed_arg()
def patched_configuration_load_config_file(mocker, config) -> None:
mocker.patch(
'freqtrade.configuration.configuration.load_config_file',
lambda *args, **kwargs: config
)
def patch_exchange(mocker, api_mock=None, id='bittrex') -> None: def patch_exchange(mocker, api_mock=None, id='bittrex') -> None:
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
@@ -227,7 +239,8 @@ def default_conf():
}, },
"initial_state": "running", "initial_state": "running",
"db_url": "sqlite://", "db_url": "sqlite://",
"loglevel": logging.DEBUG, "user_data_dir": Path("user_data"),
"verbosity": 3,
} }
return configuration return configuration
@@ -297,7 +310,7 @@ def markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': '', 'info': {},
}, },
'TKN/BTC': { 'TKN/BTC': {
'id': 'tknbtc', 'id': 'tknbtc',
@@ -322,7 +335,7 @@ def markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': '', 'info': {},
}, },
'BLK/BTC': { 'BLK/BTC': {
'id': 'blkbtc', 'id': 'blkbtc',
@@ -347,7 +360,7 @@ def markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': '', 'info': {},
}, },
'LTC/BTC': { 'LTC/BTC': {
'id': 'ltcbtc', 'id': 'ltcbtc',
@@ -372,7 +385,7 @@ def markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': '', 'info': {},
}, },
'XRP/BTC': { 'XRP/BTC': {
'id': 'xrpbtc', 'id': 'xrpbtc',
@@ -397,7 +410,7 @@ def markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': '', 'info': {},
}, },
'NEO/BTC': { 'NEO/BTC': {
'id': 'neobtc', 'id': 'neobtc',
@@ -422,7 +435,7 @@ def markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': '', 'info': {},
}, },
'BTT/BTC': { 'BTT/BTC': {
'id': 'BTTBTC', 'id': 'BTTBTC',
@@ -450,7 +463,7 @@ def markets():
'max': None 'max': None
} }
}, },
'info': "", 'info': {},
}, },
'ETH/USDT': { 'ETH/USDT': {
'id': 'USDT-ETH', 'id': 'USDT-ETH',
@@ -472,7 +485,7 @@ def markets():
} }
}, },
'active': True, 'active': True,
'info': "" 'info': {},
}, },
'LTC/USDT': { 'LTC/USDT': {
'id': 'USDT-LTC', 'id': 'USDT-LTC',
@@ -494,7 +507,7 @@ def markets():
'max': None 'max': None
} }
}, },
'info': "" 'info': {},
} }
} }

View File

@@ -1,14 +1,18 @@
from unittest.mock import MagicMock from unittest.mock import MagicMock
from arrow import Arrow
import pytest import pytest
from arrow import Arrow
from pandas import DataFrame, to_datetime from pandas import DataFrame, to_datetime
from freqtrade.arguments import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
combine_tickers_with_mean,
create_cum_profit,
extract_trades_of_period, extract_trades_of_period,
load_backtest_data, load_trades_from_db) load_backtest_data, load_trades,
from freqtrade.data.history import load_pair_history, make_testdata_path load_trades_from_db)
from freqtrade.data.history import (load_data, load_pair_history,
make_testdata_path)
from freqtrade.tests.test_persistence import create_mock_trades from freqtrade.tests.test_persistence import create_mock_trades
@@ -41,6 +45,11 @@ def test_load_trades_db(default_conf, fee, mocker):
assert isinstance(trades, DataFrame) assert isinstance(trades, DataFrame)
assert "pair" in trades.columns assert "pair" in trades.columns
assert "open_time" in trades.columns assert "open_time" in trades.columns
assert "profitperc" in trades.columns
for col in BT_DATA_COLUMNS:
if col not in ['index', 'open_at_end']:
assert col in trades.columns
def test_extract_trades_of_period(): def test_extract_trades_of_period():
@@ -74,3 +83,52 @@ def test_extract_trades_of_period():
assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime
assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime
assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime
def test_load_trades(default_conf, mocker):
db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock())
bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock())
default_conf['trade_source'] = "DB"
load_trades(default_conf)
assert db_mock.call_count == 1
assert bt_mock.call_count == 0
db_mock.reset_mock()
bt_mock.reset_mock()
default_conf['trade_source'] = "file"
default_conf['exportfilename'] = "testfile.json"
load_trades(default_conf)
assert db_mock.call_count == 0
assert bt_mock.call_count == 1
def test_combine_tickers_with_mean():
pairs = ["ETH/BTC", "XLM/BTC"]
tickers = load_data(datadir=None,
pairs=pairs,
ticker_interval='5m'
)
df = combine_tickers_with_mean(tickers)
assert isinstance(df, DataFrame)
assert "ETH/BTC" in df.columns
assert "XLM/BTC" in df.columns
assert "mean" in df.columns
def test_create_cum_profit():
filename = make_testdata_path(None) / "backtest-result_test.json"
bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112")
df = load_pair_history(pair="POWR/BTC", ticker_interval='5m',
datadir=None, timerange=timerange)
cum_profits = create_cum_profit(df.set_index('date'),
bt_data[bt_data["pair"] == 'POWR/BTC'],
"cum_profits")
assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]['cum_profits'] == 0
assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005

View File

@@ -18,7 +18,7 @@ def test_parse_ticker_dataframe(ticker_history_list, caplog):
dataframe = parse_ticker_dataframe(ticker_history_list, '5m', dataframe = parse_ticker_dataframe(ticker_history_list, '5m',
pair="UNITTEST/BTC", fill_missing=True) pair="UNITTEST/BTC", fill_missing=True)
assert dataframe.columns.tolist() == columns assert dataframe.columns.tolist() == columns
assert log_has('Parsing tickerlist to dataframe', caplog.record_tuples) assert log_has('Parsing tickerlist to dataframe', caplog)
def test_ohlcv_fill_up_missing_data(caplog): def test_ohlcv_fill_up_missing_data(caplog):
@@ -34,8 +34,7 @@ def test_ohlcv_fill_up_missing_data(caplog):
assert (data.columns == data2.columns).all() assert (data.columns == data2.columns).all()
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
f"{len(data)} - after: {len(data2)}", f"{len(data)} - after: {len(data2)}", caplog)
caplog.record_tuples)
# Test fillup actually fixes invalid backtest data # Test fillup actually fixes invalid backtest data
min_date, max_date = get_timeframe({'UNITTEST/BTC': data}) min_date, max_date = get_timeframe({'UNITTEST/BTC': data})
@@ -97,8 +96,7 @@ def test_ohlcv_fill_up_missing_data2(caplog):
assert (data.columns == data2.columns).all() assert (data.columns == data2.columns).all()
assert log_has(f"Missing data fillup for UNITTEST/BTC: before: " assert log_has(f"Missing data fillup for UNITTEST/BTC: before: "
f"{len(data)} - after: {len(data2)}", f"{len(data)} - after: {len(data2)}", caplog)
caplog.record_tuples)
def test_ohlcv_drop_incomplete(caplog): def test_ohlcv_drop_incomplete(caplog):
@@ -140,11 +138,11 @@ def test_ohlcv_drop_incomplete(caplog):
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=False) fill_missing=False, drop_incomplete=False)
assert len(data) == 4 assert len(data) == 4
assert not log_has("Dropping last candle", caplog.record_tuples) assert not log_has("Dropping last candle", caplog)
# Drop last candle # Drop last candle
data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC", data = parse_ticker_dataframe(ticks, ticker_interval, pair="UNITTEST/BTC",
fill_missing=False, drop_incomplete=True) fill_missing=False, drop_incomplete=True)
assert len(data) == 3 assert len(data) == 3
assert log_has("Dropping last candle", caplog.record_tuples) assert log_has("Dropping last candle", caplog)

View File

@@ -13,6 +13,7 @@ def test_ohlcv(mocker, default_conf, ticker_history):
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN assert dp.runmode == RunMode.DRY_RUN
assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval)) assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval))
@@ -37,11 +38,9 @@ def test_ohlcv(mocker, default_conf, ticker_history):
def test_historic_ohlcv(mocker, default_conf, ticker_history): def test_historic_ohlcv(mocker, default_conf, ticker_history):
historymock = MagicMock(return_value=ticker_history) historymock = MagicMock(return_value=ticker_history)
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
# exchange = get_patched_exchange(mocker, default_conf)
dp = DataProvider(default_conf, None) dp = DataProvider(default_conf, None)
data = dp.historic_ohlcv("UNITTEST/BTC", "5m") data = dp.historic_ohlcv("UNITTEST/BTC", "5m")
assert isinstance(data, DataFrame) assert isinstance(data, DataFrame)
@@ -51,14 +50,47 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history):
assert historymock.call_args_list[0][1]["ticker_interval"] == "5m" assert historymock.call_args_list[0][1]["ticker_interval"] == "5m"
def test_get_pair_dataframe(mocker, default_conf, ticker_history):
default_conf["runmode"] = RunMode.DRY_RUN
ticker_interval = default_conf["ticker_interval"]
exchange = get_patched_exchange(mocker, default_conf)
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.DRY_RUN
assert ticker_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval))
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ticker_history
assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
# Test with and without parameter
assert dp.get_pair_dataframe("UNITTEST/BTC",
ticker_interval).equals(dp.get_pair_dataframe("UNITTEST/BTC"))
default_conf["runmode"] = RunMode.LIVE
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.LIVE
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
historymock = MagicMock(return_value=ticker_history)
mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock)
default_conf["runmode"] = RunMode.BACKTEST
dp = DataProvider(default_conf, exchange)
assert dp.runmode == RunMode.BACKTEST
assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame)
# assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty
def test_available_pairs(mocker, default_conf, ticker_history): def test_available_pairs(mocker, default_conf, ticker_history):
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
ticker_interval = default_conf["ticker_interval"] ticker_interval = default_conf["ticker_interval"]
exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history
exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history
dp = DataProvider(default_conf, exchange)
dp = DataProvider(default_conf, exchange)
assert len(dp.available_pairs) == 2 assert len(dp.available_pairs) == 2
assert dp.available_pairs == [ assert dp.available_pairs == [
("XRP/BTC", ticker_interval), ("XRP/BTC", ticker_interval),

View File

@@ -12,7 +12,7 @@ import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.history import (download_pair_history, from freqtrade.data.history import (download_pair_history,
load_cached_data_for_updating, load_cached_data_for_updating,
@@ -64,8 +64,7 @@ def test_load_data_30min_ticker(mocker, caplog, default_conf) -> None:
assert isinstance(ld, DataFrame) assert isinstance(ld, DataFrame)
assert not log_has( assert not log_has(
'Download history data for pair: "UNITTEST/BTC", interval: 30m ' 'Download history data for pair: "UNITTEST/BTC", interval: 30m '
'and store in None.', 'and store in None.', caplog
caplog.record_tuples
) )
@@ -75,22 +74,20 @@ def test_load_data_7min_ticker(mocker, caplog, default_conf) -> None:
assert ld is None assert ld is None
assert log_has( assert log_has(
'No history data for pair: "UNITTEST/BTC", interval: 7m. ' 'No history data for pair: "UNITTEST/BTC", interval: 7m. '
'Use --refresh-pairs-cached option or download_backtest_data.py ' 'Use --refresh-pairs-cached option or `freqtrade download-data` '
'script to download the data', 'script to download the data', caplog
caplog.record_tuples
) )
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json') file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC']) history.load_data(datadir=None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
assert os.path.isfile(file) is True assert os.path.isfile(file) is True
assert not log_has( assert not log_has(
'Download history data for pair: "UNITTEST/BTC", interval: 1m ' 'Download history data for pair: "UNITTEST/BTC", interval: 1m '
'and store in None.', 'and store in None.', caplog
caplog.record_tuples
) )
_clean_test_file(file) _clean_test_file(file)
@@ -99,7 +96,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau
""" """
Test load_pair_history() with 1 min ticker Test load_pair_history() with 1 min ticker
""" """
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
@@ -112,9 +109,8 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau
assert os.path.isfile(file) is False assert os.path.isfile(file) is False
assert log_has( assert log_has(
'No history data for pair: "MEME/BTC", interval: 1m. ' 'No history data for pair: "MEME/BTC", interval: 1m. '
'Use --refresh-pairs-cached option or download_backtest_data.py ' 'Use --refresh-pairs-cached option or `freqtrade download-data` '
'script to download the data', 'script to download the data', caplog
caplog.record_tuples
) )
# download a new pair if refresh_pairs is set # download a new pair if refresh_pairs is set
@@ -126,8 +122,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, defau
assert os.path.isfile(file) is True assert os.path.isfile(file) is True
assert log_has( assert log_has(
'Download history data for pair: "MEME/BTC", interval: 1m ' 'Download history data for pair: "MEME/BTC", interval: 1m '
'and store in None.', 'and store in None.', caplog
caplog.record_tuples
) )
with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'):
history.load_pair_history(datadir=None, history.load_pair_history(datadir=None,
@@ -149,7 +144,7 @@ def test_load_data_live(default_conf, mocker, caplog) -> None:
exchange=exchange) exchange=exchange)
assert refresh_mock.call_count == 1 assert refresh_mock.call_count == 1
assert len(refresh_mock.call_args_list[0][0][0]) == 2 assert len(refresh_mock.call_args_list[0][0][0]) == 2
assert log_has('Live: Downloading data for all defined pairs ...', caplog.record_tuples) assert log_has('Live: Downloading data for all defined pairs ...', caplog)
def test_load_data_live_noexchange(default_conf, mocker, caplog) -> None: def test_load_data_live_noexchange(default_conf, mocker, caplog) -> None:
@@ -183,16 +178,13 @@ def test_load_cached_data_for_updating(mocker) -> None:
# timeframe starts earlier than the cached data # timeframe starts earlier than the cached data
# should fully update data # should fully update data
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0) timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename, data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
'1m',
timerange)
assert data == [] assert data == []
assert start_ts == test_data[0][0] - 1000 assert start_ts == test_data[0][0] - 1000
# same with 'line' timeframe # same with 'line' timeframe
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120 num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
data, start_ts = load_cached_data_for_updating(test_filename, data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m',
'1m',
TimeRange(None, 'line', 0, -num_lines)) TimeRange(None, 'line', 0, -num_lines))
assert data == [] assert data == []
assert start_ts < test_data[0][0] - 1 assert start_ts < test_data[0][0] - 1
@@ -200,36 +192,29 @@ def test_load_cached_data_for_updating(mocker) -> None:
# timeframe starts in the center of the cached data # timeframe starts in the center of the cached data
# should return the chached data w/o the last item # should return the chached data w/o the last item
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0) timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename, data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
'1m',
timerange)
assert data == test_data[:-1] assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0] assert test_data[-2][0] < start_ts < test_data[-1][0]
# same with 'line' timeframe # same with 'line' timeframe
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30 num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
timerange = TimeRange(None, 'line', 0, -num_lines) timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename, data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
'1m',
timerange)
assert data == test_data[:-1] assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0] assert test_data[-2][0] < start_ts < test_data[-1][0]
# timeframe starts after the chached data # timeframe starts after the chached data
# should return the chached data w/o the last item # should return the chached data w/o the last item
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0) timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename, data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
'1m',
timerange)
assert data == test_data[:-1] assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0] assert test_data[-2][0] < start_ts < test_data[-1][0]
# same with 'line' timeframe # Try loading last 30 lines.
# Not supported by load_cached_data_for_updating, we always need to get the full data.
num_lines = 30 num_lines = 30
timerange = TimeRange(None, 'line', 0, -num_lines) timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename, data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
'1m',
timerange)
assert data == test_data[:-1] assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0] assert test_data[-2][0] < start_ts < test_data[-1][0]
@@ -237,41 +222,33 @@ def test_load_cached_data_for_updating(mocker) -> None:
# should return the chached data w/o the last item # should return the chached data w/o the last item
num_lines = 30 num_lines = 30
timerange = TimeRange(None, 'line', 0, -num_lines) timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename, data, start_ts = load_cached_data_for_updating(datadir, 'UNITTEST/BTC', '1m', timerange)
'1m',
timerange)
assert data == test_data[:-1] assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0] assert test_data[-2][0] < start_ts < test_data[-1][0]
# no datafile exist # no datafile exist
# should return timestamp start time # should return timestamp start time
timerange = TimeRange('date', None, now_ts - 10000, 0) timerange = TimeRange('date', None, now_ts - 10000, 0)
data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'), data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange)
'1m',
timerange)
assert data == [] assert data == []
assert start_ts == (now_ts - 10000) * 1000 assert start_ts == (now_ts - 10000) * 1000
# same with 'line' timeframe # same with 'line' timeframe
num_lines = 30 num_lines = 30
timerange = TimeRange(None, 'line', 0, -num_lines) timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'), data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', timerange)
'1m',
timerange)
assert data == [] assert data == []
assert start_ts == (now_ts - num_lines * 60) * 1000 assert start_ts == (now_ts - num_lines * 60) * 1000
# no datafile exist, no timeframe is set # no datafile exist, no timeframe is set
# should return an empty array and None # should return an empty array and None
data, start_ts = load_cached_data_for_updating(test_filename.with_name('unexist'), data, start_ts = load_cached_data_for_updating(datadir, 'NONEXIST/BTC', '1m', None)
'1m',
None)
assert data == [] assert data == []
assert start_ts is None assert start_ts is None
def test_download_pair_history(ticker_history_list, mocker, default_conf) -> None: def test_download_pair_history(ticker_history_list, mocker, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history_list) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history_list)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json') file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
@@ -324,7 +301,7 @@ def test_download_pair_history2(mocker, default_conf) -> None:
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199] [1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
] ]
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='1m') download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='1m')
download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='3m') download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='3m')
@@ -332,7 +309,7 @@ def test_download_pair_history2(mocker, default_conf) -> None:
def test_download_backtesting_data_exception(ticker_history, mocker, caplog, default_conf) -> None: def test_download_backtesting_data_exception(ticker_history, mocker, caplog, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_history', mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv',
side_effect=Exception('File Error')) side_effect=Exception('File Error'))
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
@@ -350,8 +327,7 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def
_clean_test_file(file1_5) _clean_test_file(file1_5)
assert log_has( assert log_has(
'Failed to download history data for pair: "MEME/BTC", interval: 1m. ' 'Failed to download history data for pair: "MEME/BTC", interval: 1m. '
'Error: File Error', 'Error: File Error', caplog
caplog.record_tuples
) )
@@ -380,7 +356,7 @@ def test_load_partial_missing(caplog) -> None:
start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0] start_real = tickerdata['UNITTEST/BTC'].iloc[0, 0]
assert log_has(f'Missing data at start for pair ' assert log_has(f'Missing data at start for pair '
f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}', f'UNITTEST/BTC, data starts at {start_real.strftime("%Y-%m-%d %H:%M:%S")}',
caplog.record_tuples) caplog)
# Make sure we start fresh - test missing data at end # Make sure we start fresh - test missing data at end
caplog.clear() caplog.clear()
start = arrow.get('2018-01-10T00:00:00') start = arrow.get('2018-01-10T00:00:00')
@@ -396,7 +372,7 @@ def test_load_partial_missing(caplog) -> None:
end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5) end_real = arrow.get(tickerdata['UNITTEST/BTC'].iloc[-1, 0]).shift(minutes=5)
assert log_has(f'Missing data at end for pair ' assert log_has(f'Missing data at end for pair '
f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}', f'UNITTEST/BTC, data ends at {end_real.strftime("%Y-%m-%d %H:%M:%S")}',
caplog.record_tuples) caplog)
def test_init(default_conf, mocker) -> None: def test_init(default_conf, mocker) -> None:
@@ -560,7 +536,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog) -> None:
assert len(caplog.record_tuples) == 1 assert len(caplog.record_tuples) == 1
assert log_has( assert log_has(
"UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values",
caplog.record_tuples) caplog)
def test_validate_backtest_data(default_conf, mocker, caplog) -> None: def test_validate_backtest_data(default_conf, mocker, caplog) -> None:

View File

@@ -311,7 +311,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog):
assert not edge.calculate() assert not edge.calculate()
assert len(edge._cached_pairs) == 0 assert len(edge._cached_pairs) == 0
assert log_has("No data found. Edge is stopped ...", caplog.record_tuples) assert log_has("No data found. Edge is stopped ...", caplog)
assert edge._last_updated == 0 assert edge._last_updated == 0
@@ -326,7 +326,7 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog):
assert not edge.calculate() assert not edge.calculate()
assert len(edge._cached_pairs) == 0 assert len(edge._cached_pairs) == 0
assert log_has("No trades found.", caplog.record_tuples) assert log_has("No trades found.", caplog)
def test_edge_init_error(mocker, edge_conf,): def test_edge_init_error(mocker, edge_conf,):

View File

@@ -2,7 +2,7 @@
# pragma pylint: disable=protected-access # pragma pylint: disable=protected-access
import copy import copy
import logging import logging
from datetime import datetime from datetime import datetime, timezone
from random import randint from random import randint
from unittest.mock import MagicMock, Mock, PropertyMock from unittest.mock import MagicMock, Mock, PropertyMock
@@ -11,10 +11,14 @@ import ccxt
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade import (DependencyException, OperationalException, from freqtrade import (DependencyException, InvalidOrderException,
TemporaryError, InvalidOrderException) OperationalException, TemporaryError)
from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange import Binance, Exchange, Kraken
from freqtrade.exchange.exchange import API_RETRY_COUNT from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes,
timeframe_to_msecs,
timeframe_to_next_date,
timeframe_to_prev_date,
timeframe_to_seconds)
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re
@@ -33,13 +37,13 @@ def get_mock_coro(return_value):
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
fun, mock_ccxt_fun, **kwargs): fun, mock_ccxt_fun, **kwargs):
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
getattr(exchange, fun)(**kwargs) getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
getattr(exchange, fun)(**kwargs) getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
@@ -47,13 +51,13 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs) await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError) api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs) await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
@@ -62,7 +66,7 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu
def test_init(default_conf, mocker, caplog): def test_init(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
get_patched_exchange(mocker, default_conf) get_patched_exchange(mocker, default_conf)
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples) assert log_has('Instance is running with dry_run enabled', caplog)
def test_init_ccxt_kwargs(default_conf, mocker, caplog): def test_init_ccxt_kwargs(default_conf, mocker, caplog):
@@ -71,8 +75,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
conf = copy.deepcopy(default_conf) conf = copy.deepcopy(default_conf)
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True} conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
ex = Exchange(conf) ex = Exchange(conf)
assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
caplog.record_tuples)
assert ex._api_async.aiohttp_trust_env assert ex._api_async.aiohttp_trust_env
assert not ex._api.aiohttp_trust_env assert not ex._api.aiohttp_trust_env
@@ -81,20 +84,18 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
conf = copy.deepcopy(default_conf) conf = copy.deepcopy(default_conf)
conf['exchange']['ccxt_config'] = {'TestKWARG': 11} conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
ex = Exchange(conf) ex = Exchange(conf)
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
caplog.record_tuples)
assert not ex._api_async.aiohttp_trust_env assert not ex._api_async.aiohttp_trust_env
assert hasattr(ex._api, 'TestKWARG') assert hasattr(ex._api, 'TestKWARG')
assert ex._api.TestKWARG == 11 assert ex._api.TestKWARG == 11
assert not hasattr(ex._api_async, 'TestKWARG') assert not hasattr(ex._api_async, 'TestKWARG')
assert log_has("Applying additional ccxt config: {'TestKWARG': 11}", assert log_has("Applying additional ccxt config: {'TestKWARG': 11}", caplog)
caplog.record_tuples)
def test_destroy(default_conf, mocker, caplog): def test_destroy(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
get_patched_exchange(mocker, default_conf) get_patched_exchange(mocker, default_conf)
assert log_has('Exchange object destroyed, closing async loop', caplog.record_tuples) assert log_has('Exchange object destroyed, closing async loop', caplog)
def test_init_exception(default_conf, mocker): def test_init_exception(default_conf, mocker):
@@ -120,8 +121,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
exchange = ExchangeResolver('Bittrex', default_conf).exchange exchange = ExchangeResolver('Bittrex', default_conf).exchange
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
caplog.record_tuples)
caplog.clear() caplog.clear()
exchange = ExchangeResolver('kraken', default_conf).exchange exchange = ExchangeResolver('kraken', default_conf).exchange
@@ -129,7 +129,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
assert isinstance(exchange, Kraken) assert isinstance(exchange, Kraken)
assert not isinstance(exchange, Binance) assert not isinstance(exchange, Binance)
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
caplog.record_tuples) caplog)
exchange = ExchangeResolver('binance', default_conf).exchange exchange = ExchangeResolver('binance', default_conf).exchange
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
@@ -137,7 +137,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
assert not isinstance(exchange, Kraken) assert not isinstance(exchange, Kraken)
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
caplog.record_tuples) caplog)
def test_validate_order_time_in_force(default_conf, mocker, caplog): def test_validate_order_time_in_force(default_conf, mocker, caplog):
@@ -249,20 +249,19 @@ def test__load_async_markets(default_conf, mocker, caplog):
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
exchange._load_async_markets() exchange._load_async_markets()
assert log_has('Could not load async markets. Reason: deadbeef', assert log_has('Could not load async markets. Reason: deadbeef', caplog)
caplog.record_tuples)
def test__load_markets(default_conf, mocker, caplog): def test__load_markets(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError()) api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError"))
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
Exchange(default_conf) Exchange(default_conf)
assert log_has('Unable to initialize markets. Reason: ', caplog.record_tuples) assert log_has('Unable to initialize markets. Reason: SomeError', caplog)
expected_return = {'ETH/BTC': 'available'} expected_return = {'ETH/BTC': 'available'}
api_mock = MagicMock() api_mock = MagicMock()
@@ -298,27 +297,27 @@ def test__reload_markets(default_conf, mocker, caplog):
exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60 exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60
exchange._reload_markets() exchange._reload_markets()
assert exchange.markets == updated_markets assert exchange.markets == updated_markets
assert log_has('Performing scheduled market reload..', caplog.record_tuples) assert log_has('Performing scheduled market reload..', caplog)
def test__reload_markets_exception(default_conf, mocker, caplog): def test__reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError) api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError"))
default_conf['exchange']['markets_refresh_interval'] = 10 default_conf['exchange']['markets_refresh_interval'] = 10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
# less than 10 minutes have passed, no reload # less than 10 minutes have passed, no reload
exchange._reload_markets() exchange._reload_markets()
assert exchange._last_markets_refresh == 0 assert exchange._last_markets_refresh == 0
assert log_has_re(r"Could not reload markets.*", caplog.record_tuples) assert log_has_re(r"Could not reload markets.*", caplog)
def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={ type(api_mock).markets = PropertyMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' 'ETH/BTC': {}, 'LTC/BTC': {}, 'XRP/BTC': {}, 'NEO/BTC': {}
}) })
id_mock = PropertyMock(return_value='test_exchange') id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock type(api_mock).id = id_mock
@@ -332,7 +331,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d
def test_validate_pairs_not_available(default_conf, mocker): def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={ type(api_mock).markets = PropertyMock(return_value={
'XRP/BTC': 'inactive' 'XRP/BTC': {'inactive': True}
}) })
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
@@ -357,8 +356,23 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value={}))
Exchange(default_conf) Exchange(default_conf)
assert log_has('Unable to validate pairs (assuming they are correct).', assert log_has('Unable to validate pairs (assuming they are correct).', caplog)
caplog.record_tuples)
def test_validate_pairs_restricted(default_conf, mocker, caplog):
api_mock = MagicMock()
type(api_mock).markets = PropertyMock(return_value={
'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {},
'XRP/BTC': {'info': {'IsRestricted': True}}
})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
Exchange(default_conf)
assert log_has(f"Pair XRP/BTC is restricted for some users on this exchange."
f"Please check if you are impacted by this restriction "
f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog)
def test_validate_timeframes(default_conf, mocker): def test_validate_timeframes(default_conf, mocker):
@@ -396,6 +410,45 @@ def test_validate_timeframes_failed(default_conf, mocker):
Exchange(default_conf) Exchange(default_conf)
def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker):
default_conf["ticker_interval"] = "3m"
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
# delete timeframes so magicmock does not autocreate it
del api_mock.timeframes
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
with pytest.raises(OperationalException,
match=r'The ccxt library does not provide the list of timeframes '
r'for the exchange ".*" and this exchange '
r'is therefore not supported. *'):
Exchange(default_conf)
def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
default_conf["ticker_interval"] = "3m"
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
# delete timeframes so magicmock does not autocreate it
del api_mock.timeframes
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange._load_markets',
MagicMock(return_value={'timeframes': None}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
with pytest.raises(OperationalException,
match=r'The ccxt library does not provide the list of timeframes '
r'for the exchange ".*" and this exchange '
r'is therefore not supported. *'):
Exchange(default_conf)
def test_validate_timeframes_not_in_config(default_conf, mocker): def test_validate_timeframes_not_in_config(default_conf, mocker):
del default_conf["ticker_interval"] del default_conf["ticker_interval"]
api_mock = MagicMock() api_mock = MagicMock()
@@ -504,15 +557,17 @@ def test_dry_run_order(default_conf, mocker, side, exchange_name):
("buy"), ("buy"),
("sell") ("sell")
]) ])
@pytest.mark.parametrize("ordertype,rate", [ @pytest.mark.parametrize("ordertype,rate,marketprice", [
("market", None), ("market", None, None),
("limit", 200), ("market", 200, True),
("stop_loss_limit", 200) ("limit", 200, None),
("stop_loss_limit", 200, None)
]) ])
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_create_order(default_conf, mocker, side, ordertype, rate, exchange_name): def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6)) order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6))
api_mock.options = {} if not marketprice else {"createMarketBuyOrderRequiresPrice": True}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@@ -553,6 +608,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'market' order_type = 'market'
time_in_force = 'gtc' time_in_force = 'gtc'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@@ -592,25 +648,31 @@ def test_buy_prod(default_conf, mocker, exchange_name):
# test exception handling # test exception handling
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("Not enough funds"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.buy(pair='ETH/BTC', ordertype=order_type, exchange.buy(pair='ETH/BTC', ordertype=order_type,
amount=1, rate=200, time_in_force=time_in_force) amount=1, rate=200, time_in_force=time_in_force)
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.buy(pair='ETH/BTC', ordertype=order_type, exchange.buy(pair='ETH/BTC', ordertype='limit',
amount=1, rate=200, time_in_force=time_in_force)
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.buy(pair='ETH/BTC', ordertype='market',
amount=1, rate=200, time_in_force=time_in_force) amount=1, rate=200, time_in_force=time_in_force)
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("Network disconnect"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.buy(pair='ETH/BTC', ordertype=order_type, exchange.buy(pair='ETH/BTC', ordertype=order_type,
amount=1, rate=200, time_in_force=time_in_force) amount=1, rate=200, time_in_force=time_in_force)
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.buy(pair='ETH/BTC', ordertype=order_type, exchange.buy(pair='ETH/BTC', ordertype=order_type,
amount=1, rate=200, time_in_force=time_in_force) amount=1, rate=200, time_in_force=time_in_force)
@@ -620,6 +682,7 @@ def test_buy_prod(default_conf, mocker, exchange_name):
def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): def test_buy_considers_time_in_force(default_conf, mocker, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@@ -680,6 +743,7 @@ def test_sell_prod(default_conf, mocker, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
order_type = 'market' order_type = 'market'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@@ -714,22 +778,28 @@ def test_sell_prod(default_conf, mocker, exchange_name):
# test exception handling # test exception handling
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) exchange.sell(pair='ETH/BTC', ordertype='limit', amount=1, rate=200)
# Market orders don't require price, so the behaviour is slightly different
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.sell(pair='ETH/BTC', ordertype='market', amount=1, rate=200)
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
@@ -744,6 +814,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name):
'foo': 'bar' 'foo': 'bar'
} }
}) })
api_mock.options = {}
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
@@ -801,7 +872,7 @@ def test_get_balance_prod(default_conf, mocker, exchange_name):
assert exchange.get_balance(currency='BTC') == 123.4 assert exchange.get_balance(currency='BTC') == 123.4
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError) api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_balance(currency='BTC') exchange.get_balance(currency='BTC')
@@ -874,7 +945,7 @@ def test_get_tickers(default_conf, mocker, exchange_name):
"get_tickers", "fetch_tickers") "get_tickers", "fetch_tickers")
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported) api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_tickers() exchange.get_tickers()
@@ -893,7 +964,7 @@ def test_get_ticker(default_conf, mocker, exchange_name):
'last': 0.0001, 'last': 0.0001,
} }
api_mock.fetch_ticker = MagicMock(return_value=tick) api_mock.fetch_ticker = MagicMock(return_value=tick)
api_mock.markets = {'ETH/BTC': {}} api_mock.markets = {'ETH/BTC': {'active': True}}
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
# retrieve original ticker # retrieve original ticker
ticker = exchange.get_ticker(pair='ETH/BTC') ticker = exchange.get_ticker(pair='ETH/BTC')
@@ -941,7 +1012,7 @@ def test_get_ticker(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_history(default_conf, mocker, caplog, exchange_name): def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
tick = [ tick = [
[ [
@@ -962,7 +1033,7 @@ def test_get_history(default_conf, mocker, caplog, exchange_name):
# one_call calculation * 1.8 should do 2 calls # one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * 500 * 1.8 since = 5 * 60 * 500 * 1.8
print(f"since = {since}") print(f"since = {since}")
ret = exchange.get_history(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
assert exchange._async_get_candle_history.call_count == 2 assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above tick # Returns twice the above tick
@@ -998,7 +1069,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
assert not exchange._klines assert not exchange._klines
exchange.refresh_latest_ohlcv(pairs) exchange.refresh_latest_ohlcv(pairs)
assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog.record_tuples) assert log_has(f'Refreshing ohlcv data for {len(pairs)} pairs', caplog)
assert exchange._klines assert exchange._klines
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
for pair in pairs: for pair in pairs:
@@ -1017,7 +1088,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...", assert log_has(f"Using cached ohlcv data for pair {pairs[0][0]}, interval {pairs[0][1]} ...",
caplog.record_tuples) caplog)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -1047,7 +1118,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
assert res[1] == "5m" assert res[1] == "5m"
assert res[2] == tick assert res[2] == tick
assert exchange._api_async.fetch_ohlcv.call_count == 1 assert exchange._api_async.fetch_ohlcv.call_count == 1
assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog.record_tuples) assert not log_has(f"Using cached ohlcv data for {pair} ...", caplog)
# exchange = Exchange(default_conf) # exchange = Exchange(default_conf)
await async_ccxt_exception(mocker, default_conf, MagicMock(), await async_ccxt_exception(mocker, default_conf, MagicMock(),
@@ -1056,7 +1127,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_
api_mock = MagicMock() api_mock = MagicMock()
with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError) api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
await exchange._async_get_candle_history(pair, "5m", await exchange._async_get_candle_history(pair, "5m",
(arrow.utcnow().timestamp - 2000) * 1000) (arrow.utcnow().timestamp - 2000) * 1000)
@@ -1106,8 +1177,8 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
# Test that each is in list at least once as order is not guaranteed # Test that each is in list at least once as order is not guaranteed
assert type(res[0]) is tuple or type(res[1]) is tuple assert type(res[0]) is tuple or type(res[1]) is tuple
assert type(res[0]) is TypeError or type(res[1]) is TypeError assert type(res[0]) is TypeError or type(res[1]) is TypeError
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog.record_tuples) assert log_has("Error loading ETH/BTC. Result was [[]].", caplog)
assert log_has("Async code raised an exception: TypeError", caplog.record_tuples) assert log_has("Async code raised an exception: TypeError", caplog)
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
@@ -1128,15 +1199,15 @@ def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name):
def test_get_order_book_exception(default_conf, mocker, exchange_name): def test_get_order_book_exception(default_conf, mocker, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported) api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported("Not supported"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50) exchange.get_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError) api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50) exchange.get_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError) api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order_book(pair='ETH/BTC', limit=50) exchange.get_order_book(pair='ETH/BTC', limit=50)
@@ -1249,7 +1320,7 @@ def test_cancel_order(default_conf, mocker, exchange_name):
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.cancel_order(order_id='_', pair='TKN/BTC') exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == 1 assert api_mock.cancel_order.call_count == 1
@@ -1269,6 +1340,9 @@ def test_get_order(default_conf, mocker, exchange_name):
print(exchange.get_order('X', 'TKN/BTC')) print(exchange.get_order('X', 'TKN/BTC'))
assert exchange.get_order('X', 'TKN/BTC').myid == 123 assert exchange.get_order('X', 'TKN/BTC').myid == 123
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
exchange.get_order('Y', 'TKN/BTC')
default_conf['dry_run'] = False default_conf['dry_run'] = False
api_mock = MagicMock() api_mock = MagicMock()
api_mock.fetch_order = MagicMock(return_value=456) api_mock.fetch_order = MagicMock(return_value=456)
@@ -1276,7 +1350,7 @@ def test_get_order(default_conf, mocker, exchange_name):
assert exchange.get_order('X', 'TKN/BTC') == 456 assert exchange.get_order('X', 'TKN/BTC') == 456
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order(order_id='_', pair='TKN/BTC') exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1 assert api_mock.fetch_order.call_count == 1
@@ -1299,7 +1373,7 @@ def test_name(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_trades_for_order(default_conf, mocker, exchange_name): def test_get_trades_for_order(default_conf, mocker, exchange_name):
order_id = 'ABCD-ABCD' order_id = 'ABCD-ABCD'
since = datetime(2018, 5, 5) since = datetime(2018, 5, 5, tzinfo=timezone.utc)
default_conf["dry_run"] = False default_conf["dry_run"] = False
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
api_mock = MagicMock() api_mock = MagicMock()
@@ -1329,6 +1403,13 @@ def test_get_trades_for_order(default_conf, mocker, exchange_name):
orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since) orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
assert len(orders) == 1 assert len(orders) == 1
assert orders[0]['price'] == 165 assert orders[0]['price'] == 165
assert api_mock.fetch_my_trades.call_count == 1
# since argument should be
assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int)
assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC'
# Same test twice, hardcoded number and doing the same calculation
assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000
assert api_mock.fetch_my_trades.call_args[0][1] == int(since.timestamp() - 5) * 1000
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
'get_trades_for_order', 'fetch_my_trades', 'get_trades_for_order', 'fetch_my_trades',
@@ -1392,22 +1473,22 @@ def test_stoploss_limit_order(default_conf, mocker):
# test exception handling # test exception handling
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError) api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError) api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200) exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
@@ -1438,10 +1519,11 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
def test_merge_ft_has_dict(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) mocker.patch.multiple('freqtrade.exchange.Exchange',
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) _init_ccxt=MagicMock(return_value=MagicMock()),
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) _load_async_markets=MagicMock(),
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) validate_pairs=MagicMock(),
validate_timeframes=MagicMock())
ex = Exchange(default_conf) ex = Exchange(default_conf)
assert ex._ft_has == Exchange._ft_has_default assert ex._ft_has == Exchange._ft_has_default
@@ -1462,3 +1544,89 @@ def test_merge_ft_has_dict(default_conf, mocker):
assert ex._ft_has != Exchange._ft_has_default assert ex._ft_has != Exchange._ft_has_default
assert not ex._ft_has['stoploss_on_exchange'] assert not ex._ft_has['stoploss_on_exchange']
assert ex._ft_has['DeadBeef'] == 20 assert ex._ft_has['DeadBeef'] == 20
def test_get_valid_pair_combination(default_conf, mocker, markets):
mocker.patch.multiple('freqtrade.exchange.Exchange',
_init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(),
validate_pairs=MagicMock(),
validate_timeframes=MagicMock(),
markets=PropertyMock(return_value=markets))
ex = Exchange(default_conf)
assert ex.get_valid_pair_combination("ETH", "BTC") == "ETH/BTC"
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."):
ex.get_valid_pair_combination("NOPAIR", "ETH")
def test_timeframe_to_minutes():
assert timeframe_to_minutes("5m") == 5
assert timeframe_to_minutes("10m") == 10
assert timeframe_to_minutes("1h") == 60
assert timeframe_to_minutes("1d") == 1440
def test_timeframe_to_seconds():
assert timeframe_to_seconds("5m") == 300
assert timeframe_to_seconds("10m") == 600
assert timeframe_to_seconds("1h") == 3600
assert timeframe_to_seconds("1d") == 86400
def test_timeframe_to_msecs():
assert timeframe_to_msecs("5m") == 300000
assert timeframe_to_msecs("10m") == 600000
assert timeframe_to_msecs("1h") == 3600000
assert timeframe_to_msecs("1d") == 86400000
def test_timeframe_to_prev_date():
# 2019-08-12 13:22:08
date = datetime.fromtimestamp(1565616128, tz=timezone.utc)
tf_list = [
# 5m -> 2019-08-12 13:20:00
("5m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)),
# 10m -> 2019-08-12 13:20:00
("10m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)),
# 1h -> 2019-08-12 13:00:00
("1h", datetime(2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc)),
# 2h -> 2019-08-12 12:00:00
("2h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)),
# 4h -> 2019-08-12 12:00:00
("4h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)),
# 1d -> 2019-08-12 00:00:00
("1d", datetime(2019, 8, 12, 00, 00, 0, tzinfo=timezone.utc)),
]
for interval, result in tf_list:
assert timeframe_to_prev_date(interval, date) == result
date = datetime.now(tz=timezone.utc)
assert timeframe_to_prev_date("5m", date) < date
def test_timeframe_to_next_date():
# 2019-08-12 13:22:08
date = datetime.fromtimestamp(1565616128, tz=timezone.utc)
tf_list = [
# 5m -> 2019-08-12 13:25:00
("5m", datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)),
# 10m -> 2019-08-12 13:30:00
("10m", datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)),
# 1h -> 2019-08-12 14:00:00
("1h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)),
# 2h -> 2019-08-12 14:00:00
("2h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)),
# 4h -> 2019-08-12 14:00:00
("4h", datetime(2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc)),
# 1d -> 2019-08-13 00:00:00
("1d", datetime(2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc)),
]
for interval, result in tf_list:
assert timeframe_to_next_date(interval, date) == result
date = datetime.now(tz=timezone.utc)
assert timeframe_to_next_date("5m", date) > date

View File

@@ -11,6 +11,7 @@ def test_buy_kraken_trading_agreement(default_conf, mocker):
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'limit' order_type = 'limit'
time_in_force = 'ioc' time_in_force = 'ioc'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {
@@ -42,6 +43,7 @@ def test_sell_kraken_trading_agreement(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
order_type = 'market' order_type = 'market'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
'info': { 'info': {

View File

@@ -14,9 +14,8 @@ from freqtrade.tests.optimize import (BTContainer, BTrade,
_get_frame_time_from_offset, _get_frame_time_from_offset,
tests_ticker_interval) tests_ticker_interval)
# Test 0 Sell signal sell # Test 0: Sell with signal sell in candle 3
# Test with Stop-loss at 1% # Test with Stop-loss at 1%
# TC0: Sell signal in candle 3
tc0 = BTContainer(data=[ tc0 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5025, 4975, 4987, 6172, 1, 0], [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
@@ -29,9 +28,8 @@ tc0 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)]
) )
# Test 1 Minus 8% Close # Test 1: Stop-Loss Triggered 1% loss
# Test with Stop-loss at 1% # Test with Stop-loss at 1%
# TC1: Stop-Loss Triggered 1% loss
tc1 = BTContainer(data=[ tc1 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5025, 4975, 4987, 6172, 1, 0], [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
@@ -45,9 +43,8 @@ tc1 = BTContainer(data=[
) )
# Test 2 Minus 4% Low, minus 1% close # Test 2: Minus 4% Low, minus 1% close
# Test with Stop-Loss at 3% # Test with Stop-Loss at 3%
# TC2: Stop-Loss Triggered 3% Loss
tc2 = BTContainer(data=[ tc2 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5025, 4975, 4987, 6172, 1, 0], [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
@@ -61,12 +58,12 @@ tc2 = BTContainer(data=[
) )
# Test 3 Candle drops 4%, Recovers 1%. # Test 3: Multiple trades.
# Entry Criteria Met # Candle drops 4%, Recovers 1%.
# Candle drops 20% # Entry Criteria Met
# Test with Stop-Loss at 2% # Candle drops 20%
# TC3: Trade-A: Stop-Loss Triggered 2% Loss # Trade-A: Stop-Loss Triggered 2% Loss
# Trade-B: Stop-Loss Triggered 2% Loss # Trade-B: Stop-Loss Triggered 2% Loss
tc3 = BTContainer(data=[ tc3 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5025, 4975, 4987, 6172, 1, 0], [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
@@ -81,10 +78,10 @@ tc3 = BTContainer(data=[
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)] BTrade(sell_reason=SellType.STOP_LOSS, open_tick=4, close_tick=5)]
) )
# Test 4 Minus 3% / recovery +15% # Test 4: Minus 3% / recovery +15%
# Candle Data for test 3 Candle drops 3% Closed 15% up # Candle Data for test 3 Candle drops 3% Closed 15% up
# Test with Stop-loss at 2% ROI 6% # Test with Stop-loss at 2% ROI 6%
# TC4: Stop-Loss Triggered 2% Loss # Stop-Loss Triggered 2% Loss
tc4 = BTContainer(data=[ tc4 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5025, 4975, 4987, 6172, 1, 0], [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
@@ -97,9 +94,8 @@ tc4 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
) )
# Test 5 / Drops 0.5% Closes +20% # Test 5: Drops 0.5% Closes +20%, ROI triggers 3% Gain
# Set stop-loss at 1% ROI 3% # stop-loss: 1%, ROI: 3%
# TC5: ROI triggers 3% Gain
tc5 = BTContainer(data=[ tc5 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5025, 4980, 4987, 6172, 1, 0], [0, 5000, 5025, 4980, 4987, 6172, 1, 0],
@@ -112,9 +108,8 @@ tc5 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)]
) )
# Test 6 / Drops 3% / Recovers 6% Positive / Closes 1% positve # Test 6: Drops 3% / Recovers 6% Positive / Closes 1% positve, Stop-Loss triggers 2% Loss
# Set stop-loss at 2% ROI at 5% # stop-loss: 2% ROI: 5%
# TC6: Stop-Loss triggers 2% Loss
tc6 = BTContainer(data=[ tc6 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5025, 4975, 4987, 6172, 1, 0], [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
@@ -127,9 +122,8 @@ tc6 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)]
) )
# Test 7 - 6% Positive / 1% Negative / Close 1% Positve # Test 7: 6% Positive / 1% Negative / Close 1% Positve, ROI Triggers 3% Gain
# Set stop-loss at 2% ROI at 3% # stop-loss: 2% ROI: 3%
# TC7: ROI Triggers 3% Gain
tc7 = BTContainer(data=[ tc7 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5025, 4975, 4987, 6172, 1, 0], [0, 5000, 5025, 4975, 4987, 6172, 1, 0],
@@ -143,9 +137,8 @@ tc7 = BTContainer(data=[
) )
# Test 8 - trailing_stop should raise so candle 3 causes a stoploss. # Test 8: trailing_stop should raise so candle 3 causes a stoploss.
# Set stop-loss at 10%, ROI at 10% (should not apply) # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 2
# TC8: Trailing stoploss - stoploss should be adjusted candle 2
tc8 = BTContainer(data=[ tc8 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
@@ -158,10 +151,8 @@ tc8 = BTContainer(data=[
) )
# Test 9 - trailing_stop should raise - high and low in same candle. # Test 9: trailing_stop should raise - high and low in same candle.
# Candle Data for test 9 # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted in candle 3
# Set stop-loss at 10%, ROI at 10% (should not apply)
# TC9: Trailing stoploss - stoploss should be adjusted candle 3
tc9 = BTContainer(data=[ tc9 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
@@ -173,10 +164,9 @@ tc9 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
) )
# Test 10 - trailing_stop should raise so candle 3 causes a stoploss # Test 10: trailing_stop should raise so candle 3 causes a stoploss
# without applying trailing_stop_positive since stoploss_offset is at 10%. # without applying trailing_stop_positive since stoploss_offset is at 10%.
# Set stop-loss at 10%, ROI at 10% (should not apply) # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
# TC10: Trailing stoploss - stoploss should be adjusted candle 2
tc10 = BTContainer(data=[ tc10 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
@@ -190,10 +180,9 @@ tc10 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)] trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=4)]
) )
# Test 11 - trailing_stop should raise so candle 3 causes a stoploss # Test 11: trailing_stop should raise so candle 3 causes a stoploss
# applying a positive trailing stop of 3% since stop_positive_offset is reached. # applying a positive trailing stop of 3% since stop_positive_offset is reached.
# Set stop-loss at 10%, ROI at 10% (should not apply) # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
# TC11: Trailing stoploss - stoploss should be adjusted candle 2,
tc11 = BTContainer(data=[ tc11 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
@@ -207,10 +196,9 @@ tc11 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
) )
# Test 12 - trailing_stop should raise in candle 2 and cause a stoploss in the same candle # Test 12: trailing_stop should raise in candle 2 and cause a stoploss in the same candle
# applying a positive trailing stop of 3% since stop_positive_offset is reached. # applying a positive trailing stop of 3% since stop_positive_offset is reached.
# Set stop-loss at 10%, ROI at 10% (should not apply) # stop-loss: 10%, ROI: 10% (should not apply), stoploss adjusted candle 2
# TC12: Trailing stoploss - stoploss should be adjusted candle 2,
tc12 = BTContainer(data=[ tc12 = BTContainer(data=[
# D O H L C V B S # D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0], [0, 5000, 5050, 4950, 5000, 6172, 1, 0],
@@ -224,6 +212,47 @@ tc12 = BTContainer(data=[
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)]
) )
# Test 13: Buy and sell ROI on same candle
# stop-loss: 10% (should not apply), ROI: 1%
tc13 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5100, 4950, 5100, 6172, 0, 0],
[2, 5100, 5251, 4850, 5100, 6172, 0, 0],
[3, 4850, 5050, 4850, 4750, 6172, 0, 0],
[4, 4750, 4950, 4850, 4750, 6172, 0, 0]],
stop_loss=-0.10, roi=0.01, profit_perc=0.01,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)]
)
# Test 14 - Buy and Stoploss on same candle
# stop-loss: 5%, ROI: 10% (should not apply)
tc14 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5100, 4600, 5100, 6172, 0, 0],
[2, 5100, 5251, 4850, 5100, 6172, 0, 0],
[3, 4850, 5050, 4850, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.05, roi=0.10, profit_perc=-0.05,
trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)]
)
# Test 15 - Buy and ROI on same candle, followed by buy and Stoploss on next candle
# stop-loss: 5%, ROI: 10% (should not apply)
tc15 = BTContainer(data=[
# D O H L C V B S
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
[1, 5000, 5100, 4900, 5100, 6172, 1, 0],
[2, 5100, 5251, 4650, 5100, 6172, 0, 0],
[3, 4850, 5050, 4850, 4750, 6172, 0, 0],
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
stop_loss=-0.05, roi=0.01, profit_perc=-0.04,
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1),
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)]
)
TESTS = [ TESTS = [
tc0, tc0,
tc1, tc1,
@@ -238,6 +267,9 @@ TESTS = [
tc10, tc10,
tc11, tc11,
tc12, tc12,
tc13,
tc14,
tc15,
] ]

View File

@@ -1,8 +1,8 @@
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, unused-argument
import json
import math import math
import random import random
from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
import numpy as np import numpy as np
@@ -10,8 +10,8 @@ import pandas as pd
import pytest import pytest
from arrow import Arrow from arrow import Arrow
from freqtrade import DependencyException, constants from freqtrade import DependencyException, OperationalException, constants
from freqtrade.arguments import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.btanalysis import evaluate_result_multi
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
@@ -22,7 +22,9 @@ from freqtrade.optimize.backtesting import Backtesting
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange from freqtrade.tests.conftest import (get_args, log_has, log_has_re,
patch_exchange,
patched_configuration_load_config_file)
def trim_dictlist(dict_list, num): def trim_dictlist(dict_list, num):
@@ -165,9 +167,7 @@ def _trend_alternate(dataframe=None, metadata=None):
# Unit tests # Unit tests
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@@ -182,21 +182,15 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'exchange' in config assert 'exchange' in config
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config assert 'ticker_interval' in config
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples) assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
assert 'live' not in config
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'position_stacking' not in config assert 'position_stacking' not in config
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
assert 'refresh_pairs' not in config assert 'refresh_pairs' not in config
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
assert 'timerange' not in config assert 'timerange' not in config
assert 'export' not in config assert 'export' not in config
@@ -204,11 +198,13 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert config['runmode'] == RunMode.BACKTEST assert config['runmode'] == RunMode.BACKTEST
@pytest.mark.filterwarnings("ignore:DEPRECATED")
def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf) mocker.patch(
)) 'freqtrade.configuration.configuration.create_datadir',
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) lambda c, x: x
)
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@@ -216,7 +212,6 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'backtesting', 'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--live',
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
'--refresh-pairs-cached', '--refresh-pairs-cached',
@@ -234,51 +229,34 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) ->
assert 'datadir' in config assert 'datadir' in config
assert config['runmode'] == RunMode.BACKTEST assert config['runmode'] == RunMode.BACKTEST
assert log_has( assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
caplog.record_tuples) caplog)
assert 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'position_stacking' in config assert 'position_stacking' in config
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) assert log_has('Parameter --enable-position-stacking detected ...', caplog)
assert 'use_max_market_positions' in config assert 'use_max_market_positions' in config
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples) assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples) assert log_has('max_open_trades set to unlimited ...', caplog)
assert 'refresh_pairs' in config assert 'refresh_pairs' in config
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
assert 'timerange' in config assert 'timerange' in config
assert log_has( assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
assert 'export' in config assert 'export' in config
assert log_has( assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
assert 'exportfilename' in config assert 'exportfilename' in config
assert log_has( assert log_has('Storing backtest results to {} ...'.format(config['exportfilename']), caplog)
'Storing backtest results to {} ...'.format(config['exportfilename']),
caplog.record_tuples
)
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@@ -295,9 +273,8 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
@@ -305,10 +282,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
] ]
args = get_args(args) args = get_args(args)
start_backtesting(args) start_backtesting(args)
assert log_has( assert log_has('Starting freqtrade in Backtesting mode', caplog)
'Starting freqtrade in Backtesting mode',
caplog.record_tuples
)
assert start_mock.call_count == 1 assert start_mock.call_count == 1
@@ -348,6 +322,23 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None:
assert not backtesting.strategy.order_types["stoploss_on_exchange"] assert not backtesting.strategy.order_types["stoploss_on_exchange"]
def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None:
"""
Check that stoploss_on_exchange is set to False while backtesting
since backtesting assumes a perfect stoploss anyway.
"""
patch_exchange(mocker)
del default_conf['ticker_interval']
default_conf['strategy_list'] = ['DefaultStrategy',
'TestStrategy']
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
with pytest.raises(OperationalException):
Backtesting(default_conf)
log_has("Ticker-interval needs to be set in either configuration "
"or as cli argument `--ticker-interval 5m`", caplog)
def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None: def test_tickerdata_to_dataframe_bt(default_conf, mocker) -> None:
patch_exchange(mocker) patch_exchange(mocker)
timerange = TimeRange(None, 'line', 0, -100) timerange = TimeRange(None, 'line', 0, -100)
@@ -481,7 +472,6 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
default_conf['ticker_interval'] = '1m' default_conf['ticker_interval'] = '1m'
default_conf['live'] = False
default_conf['datadir'] = None default_conf['datadir'] = None
default_conf['export'] = None default_conf['export'] = None
default_conf['timerange'] = '-100' default_conf['timerange'] = '-100'
@@ -496,7 +486,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
'up to 2017-11-14T22:59:00+00:00 (0 days)..' 'up to 2017-11-14T22:59:00+00:00 (0 days)..'
] ]
for line in exists: for line in exists:
assert log_has(line, caplog.record_tuples) assert log_has(line, caplog)
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
@@ -515,7 +505,6 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
default_conf['ticker_interval'] = "1m" default_conf['ticker_interval'] = "1m"
default_conf['live'] = False
default_conf['datadir'] = None default_conf['datadir'] = None
default_conf['export'] = None default_conf['export'] = None
default_conf['timerange'] = '20180101-20180102' default_conf['timerange'] = '20180101-20180102'
@@ -524,7 +513,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
backtesting.start() backtesting.start()
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
assert log_has('No data found. Terminating.', caplog.record_tuples) assert log_has('No data found. Terminating.', caplog)
def test_backtest(default_conf, fee, mocker) -> None: def test_backtest(default_conf, fee, mocker) -> None:
@@ -621,8 +610,9 @@ def test_processed(default_conf, mocker) -> None:
def test_backtest_pricecontours(default_conf, fee, mocker) -> None: def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
# TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
tests = [['raise', 19], ['lower', 0], ['sine', 18]] tests = [['raise', 19], ['lower', 0], ['sine', 35]]
# We need to enable sell-signal - otherwise it sells on ROI!! # We need to enable sell-signal - otherwise it sells on ROI!!
default_conf['experimental'] = {"use_sell_signal": True} default_conf['experimental'] = {"use_sell_signal": True}
@@ -787,10 +777,10 @@ def test_backtest_record(default_conf, fee, mocker):
# reset test to test with strategy name # reset test to test with strategy name
names = [] names = []
records = [] records = []
backtesting._store_backtest_result("backtest-result.json", results, "DefStrat") backtesting._store_backtest_result(Path("backtest-result.json"), results, "DefStrat")
assert len(results) == 4 assert len(results) == 4
# Assert file_dump_json was only called once # Assert file_dump_json was only called once
assert names == ['backtest-result-DefStrat.json'] assert names == [Path('backtest-result-DefStrat.json')]
records = records[0] records = records[0]
# Ensure records are of correct type # Ensure records are of correct type
assert len(records) == 4 assert len(records) == 4
@@ -816,7 +806,7 @@ def test_backtest_record(default_conf, fee, mocker):
assert dur > 0 assert dur > 0
def test_backtest_start_live(default_conf, mocker, caplog): def test_backtest_start_timerange(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
async def load_pairs(pair, timeframe, since): async def load_pairs(pair, timeframe, since):
@@ -828,9 +818,7 @@ def test_backtest_start_live(default_conf, mocker, caplog):
patch_exchange(mocker, api_mock) patch_exchange(mocker, api_mock)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@@ -838,7 +826,6 @@ def test_backtest_start_live(default_conf, mocker, caplog):
'--datadir', 'freqtrade/tests/testdata', '--datadir', 'freqtrade/tests/testdata',
'backtesting', 'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--live',
'--timerange', '-100', '--timerange', '-100',
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions' '--disable-max-market-positions'
@@ -848,20 +835,18 @@ def test_backtest_start_live(default_conf, mocker, caplog):
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
exists = [ exists = [
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
'Parameter -l/--live detected ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: -100 ...', 'Parameter --timerange detected: -100 ...',
'Using data folder: freqtrade/tests/testdata ...', 'Using data directory: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...', 'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...', 'Using stake_amount: 0.001 ...',
'Live: Downloading data for all defined pairs ...', 'Backtesting with data from 2017-11-14T21:17:00+00:00 '
'Backtesting with data from 2017-11-14T19:31:00+00:00 '
'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Parameter --enable-position-stacking detected ...' 'Parameter --enable-position-stacking detected ...'
] ]
for line in exists: for line in exists:
assert log_has(line, caplog.record_tuples) assert log_has(line, caplog)
def test_backtest_start_multi_strat(default_conf, mocker, caplog): def test_backtest_start_multi_strat(default_conf, mocker, caplog):
@@ -880,16 +865,13 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
gen_strattable_mock = MagicMock() gen_strattable_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy',
gen_strattable_mock) gen_strattable_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
'--datadir', 'freqtrade/tests/testdata', '--datadir', 'freqtrade/tests/testdata',
'backtesting', 'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--live',
'--timerange', '-100', '--timerange', '-100',
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
@@ -907,14 +889,12 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
exists = [ exists = [
'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
'Parameter -l/--live detected ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: -100 ...', 'Parameter --timerange detected: -100 ...',
'Using data folder: freqtrade/tests/testdata ...', 'Using data directory: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...', 'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...', 'Using stake_amount: 0.001 ...',
'Live: Downloading data for all defined pairs ...', 'Backtesting with data from 2017-11-14T21:17:00+00:00 '
'Backtesting with data from 2017-11-14T19:31:00+00:00 '
'up to 2017-11-14T22:58:00+00:00 (0 days)..', 'up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Parameter --enable-position-stacking detected ...', 'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy DefaultStrategy',
@@ -922,4 +902,4 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog):
] ]
for line in exists: for line in exists:
assert log_has(line, caplog.record_tuples) assert log_has(line, caplog)

View File

@@ -1,20 +1,21 @@
# pragma pylint: disable=missing-docstring, C0103, C0330 # pragma pylint: disable=missing-docstring, C0103, C0330
# pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
import json
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize import setup_configuration, start_edge
from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.optimize.edge_cli import EdgeCli
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange from freqtrade.tests.conftest import (get_args, log_has, log_has_re,
patch_exchange,
patched_configuration_load_config_file)
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@@ -31,25 +32,24 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'exchange' in config assert 'exchange' in config
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config assert 'ticker_interval' in config
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples) assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
assert 'refresh_pairs' not in config assert 'refresh_pairs' not in config
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
assert 'timerange' not in config assert 'timerange' not in config
assert 'stoploss_range' not in config assert 'stoploss_range' not in config
@pytest.mark.filterwarnings("ignore:DEPRECATED")
def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None: def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, edge_conf)
read_data=json.dumps(edge_conf) mocker.patch(
)) 'freqtrade.configuration.configuration.create_datadir',
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) lambda c, x: x
)
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@@ -70,21 +70,15 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert config['runmode'] == RunMode.EDGE assert config['runmode'] == RunMode.EDGE
assert log_has( assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
caplog.record_tuples) caplog)
assert 'refresh_pairs' in config assert 'refresh_pairs' in config
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
assert 'timerange' in config assert 'timerange' in config
assert log_has( assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
def test_start(mocker, fee, edge_conf, caplog) -> None: def test_start(mocker, fee, edge_conf, caplog) -> None:
@@ -92,9 +86,8 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock) mocker.patch('freqtrade.optimize.edge_cli.EdgeCli.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, edge_conf)
read_data=json.dumps(edge_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
@@ -102,10 +95,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None:
] ]
args = get_args(args) args = get_args(args)
start_edge(args) start_edge(args)
assert log_has( assert log_has('Starting freqtrade in Edge mode', caplog)
'Starting freqtrade in Edge mode',
caplog.record_tuples
)
assert start_mock.call_count == 1 assert start_mock.call_count == 1

View File

@@ -1,30 +1,51 @@
# pragma pylint: disable=missing-docstring,W0212,C0103 # pragma pylint: disable=missing-docstring,W0212,C0103
import json
import os import os
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock, PropertyMock
from filelock import Timeout
import pandas as pd import pandas as pd
import pytest import pytest
from arrow import Arrow
from filelock import Timeout
from pathlib import Path
from freqtrade import DependencyException from freqtrade import DependencyException, OperationalException
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.history import load_tickerdata_file from freqtrade.data.history import load_tickerdata_file
from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
from freqtrade.optimize.hyperopt import Hyperopt, HYPEROPT_LOCKFILE
from freqtrade.optimize import setup_configuration, start_hyperopt from freqtrade.optimize import setup_configuration, start_hyperopt
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.optimize.default_hyperopt import DefaultHyperOpts
from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss
from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.tests.conftest import get_args, log_has, log_has_re, patch_exchange from freqtrade.strategy.interface import SellType
from freqtrade.tests.conftest import (get_args, log_has, log_has_re,
patch_exchange,
patched_configuration_load_config_file)
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def hyperopt(default_conf, mocker): def hyperopt(default_conf, mocker):
default_conf.update({'spaces': ['all']})
patch_exchange(mocker) patch_exchange(mocker)
return Hyperopt(default_conf) return Hyperopt(default_conf)
@pytest.fixture(scope='function')
def hyperopt_results():
return pd.DataFrame(
{
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2, 0.3],
'profit_abs': [0.2, 0.4, 0.5],
'trade_duration': [10, 30, 10],
'profit': [2, 0, 0],
'loss': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
}
)
# Functions for recurrent object patching # Functions for recurrent object patching
def create_trials(mocker, hyperopt) -> None: def create_trials(mocker, hyperopt) -> None:
""" """
@@ -33,20 +54,21 @@ def create_trials(mocker, hyperopt) -> None:
- we might have a pickle'd file so make sure that we return - we might have a pickle'd file so make sure that we return
false when looking for it false when looking for it
""" """
hyperopt.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') hyperopt.trials_file = Path('freqtrade/tests/optimize/ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False) mocker.patch.object(Path, "is_file", MagicMock(return_value=False))
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1) stat_mock = MagicMock()
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True) stat_mock.st_size = PropertyMock(return_value=1)
mocker.patch.object(Path, "stat", MagicMock(return_value=False))
mocker.patch.object(Path, "unlink", MagicMock(return_value=True))
mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
return [{'loss': 1, 'result': 'foo', 'params': {}}] return [{'loss': 1, 'result': 'foo', 'params': {}}]
def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@@ -60,32 +82,28 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca
assert 'exchange' in config assert 'exchange' in config
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config assert 'ticker_interval' in config
assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog.record_tuples) assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog)
assert 'live' not in config
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'position_stacking' not in config assert 'position_stacking' not in config
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
assert 'refresh_pairs' not in config assert 'refresh_pairs' not in config
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
assert 'timerange' not in config assert 'timerange' not in config
assert 'runmode' in config assert 'runmode' in config
assert config['runmode'] == RunMode.HYPEROPT assert config['runmode'] == RunMode.HYPEROPT
@pytest.mark.filterwarnings("ignore:DEPRECATED")
def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf) mocker.patch(
)) 'freqtrade.configuration.configuration.create_datadir',
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) lambda c, x: x
)
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
@@ -110,49 +128,37 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
assert 'datadir' in config assert 'datadir' in config
assert config['runmode'] == RunMode.HYPEROPT assert config['runmode'] == RunMode.HYPEROPT
assert log_has( assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
caplog.record_tuples) caplog)
assert 'position_stacking' in config assert 'position_stacking' in config
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) assert log_has('Parameter --enable-position-stacking detected ...', caplog)
assert 'use_max_market_positions' in config assert 'use_max_market_positions' in config
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples) assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples) assert log_has('max_open_trades set to unlimited ...', caplog)
assert 'refresh_pairs' in config assert 'refresh_pairs' in config
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
assert 'timerange' in config assert 'timerange' in config
assert log_has( assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
assert 'epochs' in config assert 'epochs' in config
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 1000 epochs ...', assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 1000 epochs ...',
caplog.record_tuples) caplog)
assert 'spaces' in config assert 'spaces' in config
assert log_has( assert log_has('Parameter -s/--spaces detected: {}'.format(config['spaces']), caplog)
'Parameter -s/--spaces detected: {}'.format(config['spaces']),
caplog.record_tuples
)
assert 'print_all' in config assert 'print_all' in config
assert log_has('Parameter --print-all detected ...', caplog.record_tuples) assert log_has('Parameter --print-all detected ...', caplog)
def test_hyperoptresolver(mocker, default_conf, caplog) -> None: def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
patched_configuration_load_config_file(mocker, default_conf)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
hyperopts = DefaultHyperOpts hyperopts = DefaultHyperOpts
delattr(hyperopts, 'populate_buy_trend') delattr(hyperopts, 'populate_buy_trend')
delattr(hyperopts, 'populate_sell_trend') delattr(hyperopts, 'populate_sell_trend')
@@ -164,18 +170,40 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_buy_trend')
assert not hasattr(x, 'populate_sell_trend') assert not hasattr(x, 'populate_sell_trend')
assert log_has("Custom Hyperopt does not provide populate_sell_trend. " assert log_has("Custom Hyperopt does not provide populate_sell_trend. "
"Using populate_sell_trend from DefaultStrategy.", caplog.record_tuples) "Using populate_sell_trend from DefaultStrategy.", caplog)
assert log_has("Custom Hyperopt does not provide populate_buy_trend. " assert log_has("Custom Hyperopt does not provide populate_buy_trend. "
"Using populate_buy_trend from DefaultStrategy.", caplog.record_tuples) "Using populate_buy_trend from DefaultStrategy.", caplog)
assert hasattr(x, "ticker_interval") assert hasattr(x, "ticker_interval")
def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None:
default_conf.update({'hyperopt': "NonExistingHyperoptClass"})
with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'):
HyperOptResolver(default_conf, ).hyperopt
def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
hl = DefaultHyperOptLoss
mocker.patch(
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss',
MagicMock(return_value=hl)
)
x = HyperOptLossResolver(default_conf, ).hyperoptloss
assert hasattr(x, "hyperopt_loss_function")
def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None:
default_conf.update({'hyperopt_loss': "NonExistingLossClass"})
with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'):
HyperOptLossResolver(default_conf, ).hyperopt
def test_start(mocker, default_conf, caplog) -> None: def test_start(mocker, default_conf, caplog) -> None:
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
patch_exchange(mocker) patch_exchange(mocker)
@@ -190,18 +218,12 @@ def test_start(mocker, default_conf, caplog) -> None:
import pprint import pprint
pprint.pprint(caplog.record_tuples) pprint.pprint(caplog.record_tuples)
assert log_has( assert log_has('Starting freqtrade in Hyperopt mode', caplog)
'Starting freqtrade in Hyperopt mode',
caplog.record_tuples
)
assert start_mock.call_count == 1 assert start_mock.call_count == 1
def test_start_no_data(mocker, default_conf, caplog) -> None: def test_start_no_data(mocker, default_conf, caplog) -> None:
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={})) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock(return_value={}))
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe', 'freqtrade.optimize.hyperopt.get_timeframe',
@@ -221,15 +243,12 @@ def test_start_no_data(mocker, default_conf, caplog) -> None:
import pprint import pprint
pprint.pprint(caplog.record_tuples) pprint.pprint(caplog.record_tuples)
assert log_has('No data found. Terminating.', caplog.record_tuples) assert log_has('No data found. Terminating.', caplog)
def test_start_failure(mocker, default_conf, caplog) -> None: def test_start_failure(mocker, default_conf, caplog) -> None:
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
patch_exchange(mocker) patch_exchange(mocker)
@@ -242,18 +261,12 @@ def test_start_failure(mocker, default_conf, caplog) -> None:
args = get_args(args) args = get_args(args)
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
start_hyperopt(args) start_hyperopt(args)
assert log_has( assert log_has("Please don't use --strategy for hyperopt.", caplog)
"Please don't use --strategy for hyperopt.",
caplog.record_tuples
)
def test_start_filelock(mocker, default_conf, caplog) -> None: def test_start_filelock(mocker, default_conf, caplog) -> None:
start_mock = MagicMock(side_effect=Timeout(HYPEROPT_LOCKFILE)) start_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(default_conf)))
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
patch_exchange(mocker) patch_exchange(mocker)
@@ -264,44 +277,87 @@ def test_start_filelock(mocker, default_conf, caplog) -> None:
] ]
args = get_args(args) args = get_args(args)
start_hyperopt(args) start_hyperopt(args)
assert log_has( assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog)
"Another running instance of freqtrade Hyperopt detected.",
caplog.record_tuples
)
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None:
hl = HyperOptLossResolver(default_conf).hyperoptloss
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) correct = hl.hyperopt_loss_function(hyperopt_results, 600)
over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20) over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100)
under = hyperopt.calculate_loss(1, hyperopt.target_trades - 100, 20) under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100)
assert over > correct assert over > correct
assert under > correct assert under > correct
def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None: def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None:
shorter = hyperopt.calculate_loss(1, 100, 20) resultsb = hyperopt_results.copy()
longer = hyperopt.calculate_loss(1, 100, 30) resultsb.loc[1, 'trade_duration'] = 20
hl = HyperOptLossResolver(default_conf).hyperoptloss
longer = hl.hyperopt_loss_function(hyperopt_results, 100)
shorter = hl.hyperopt_loss_function(resultsb, 100)
assert shorter < longer assert shorter < longer
def test_loss_calculation_has_limited_profit(hyperopt) -> None: def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> None:
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) results_over = hyperopt_results.copy()
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) results_under = hyperopt_results.copy()
assert over == correct results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
hl = HyperOptLossResolver(default_conf).hyperoptloss
correct = hl.hyperopt_loss_function(hyperopt_results, 600)
over = hl.hyperopt_loss_function(results_over, 600)
under = hl.hyperopt_loss_function(results_under, 600)
assert over < correct
assert under > correct
def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
results_under = hyperopt_results.copy()
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'})
hl = HyperOptLossResolver(default_conf).hyperoptloss
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1))
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1))
assert over < correct
assert under > correct
def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
results_over = hyperopt_results.copy()
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
results_under = hyperopt_results.copy()
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'})
hl = HyperOptLossResolver(default_conf).hyperoptloss
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1))
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1))
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
datetime(2019, 1, 1), datetime(2019, 5, 1))
assert over < correct
assert under > correct assert under > correct
def test_log_results_if_loss_improves(hyperopt, capsys) -> None: def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
hyperopt.current_best_loss = 2 hyperopt.current_best_loss = 2
hyperopt.total_epochs = 2
hyperopt.log_results( hyperopt.log_results(
{ {
'loss': 1, 'loss': 1,
'current_tries': 1, 'current_epoch': 1,
'total_tries': 2, 'results_explanation': 'foo.',
'result': 'foo.', 'is_initial_point': False
'initial_point': False
} }
) )
out, err = capsys.readouterr() out, err = capsys.readouterr()
@@ -325,10 +381,7 @@ def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
hyperopt.save_trials() hyperopt.save_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has( assert log_has('Saving 1 evaluations to \'{}\''.format(trials_file), caplog)
'Saving 1 evaluations to \'{}\''.format(trials_file),
caplog.record_tuples
)
mock_dump.assert_called_once() mock_dump.assert_called_once()
@@ -337,10 +390,7 @@ def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials) mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
hyperopt_trial = hyperopt.read_trials() hyperopt_trial = hyperopt.read_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has( assert log_has('Reading Trials from \'{}\''.format(trials_file), caplog)
'Reading Trials from \'{}\''.format(trials_file),
caplog.record_tuples
)
assert hyperopt_trial == trials assert hyperopt_trial == trials
mock_load.assert_called_once() mock_load.assert_called_once()
@@ -358,7 +408,7 @@ def test_roi_table_generation(hyperopt) -> None:
assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch( mocker.patch(
@@ -368,7 +418,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
parallel = mocker.patch( parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}]) MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
) )
patch_exchange(mocker) patch_exchange(mocker)
@@ -379,14 +429,23 @@ def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
'hyperopt_jobs': 1, }) 'hyperopt_jobs': 1, })
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
hyperopt.strategy.tickerdata_to_dataframe = MagicMock() hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start() hyperopt.start()
parallel.assert_called_once() parallel.assert_called_once()
assert log_has('Best result:\nfoo result\nwith values:\n', caplog.record_tuples)
out, err = capsys.readouterr()
assert 'Best result:\n\n* 1/1: foo result Objective: 1.00000\n' in out
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for tickerdata, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
assert hasattr(hyperopt.backtesting, "advise_sell")
assert hasattr(hyperopt.backtesting, "advise_buy")
assert hasattr(hyperopt, "max_open_trades")
assert hyperopt.max_open_trades == default_conf['max_open_trades']
assert hasattr(hyperopt, "position_stacking")
def test_format_results(hyperopt): def test_format_results(hyperopt):
@@ -429,7 +488,7 @@ def test_populate_indicators(hyperopt) -> None:
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
fill_missing=True)} fill_missing=True)}
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@@ -443,7 +502,7 @@ def test_buy_strategy_generator(hyperopt) -> None:
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC", tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, '1m', pair="UNITTEST/BTC",
fill_missing=True)} fill_missing=True)}
dataframes = hyperopt.strategy.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.backtesting.strategy.tickerdata_to_dataframe(tickerlist)
dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
{'pair': 'UNITTEST/BTC'}) {'pair': 'UNITTEST/BTC'})
@@ -479,12 +538,12 @@ def test_generate_optimizer(mocker, default_conf) -> None:
backtest_result = pd.DataFrame.from_records(trades, columns=labels) backtest_result = pd.DataFrame.from_records(trades, columns=labels)
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.backtest', 'freqtrade.optimize.hyperopt.Backtesting.backtest',
MagicMock(return_value=backtest_result) MagicMock(return_value=backtest_result)
) )
mocker.patch( mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe', 'freqtrade.optimize.hyperopt.get_timeframe',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13)))
) )
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
@@ -518,11 +577,119 @@ def test_generate_optimizer(mocker, default_conf) -> None:
} }
response_expected = { response_expected = {
'loss': 1.9840569076926293, 'loss': 1.9840569076926293,
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' 'results_explanation': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
'( 2.31Σ%). Avg duration 100.0 mins.', '( 2.31Σ%). Avg duration 100.0 mins.',
'params': optimizer_param 'params': optimizer_param,
'total_profit': 0.00023300
} }
hyperopt = Hyperopt(default_conf) hyperopt = Hyperopt(default_conf)
generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
assert generate_optimizer_value == response_expected assert generate_optimizer_value == response_expected
def test_clean_hyperopt(mocker, default_conf, caplog):
patch_exchange(mocker)
default_conf.update({'config': 'config.json.example',
'epochs': 1,
'timerange': None,
'spaces': 'all',
'hyperopt_jobs': 1,
})
mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
h = Hyperopt(default_conf)
assert unlinkmock.call_count == 2
assert log_has(f"Removing `{h.tickerdata_pickle}`.", caplog)
def test_continue_hyperopt(mocker, default_conf, caplog):
patch_exchange(mocker)
default_conf.update({'config': 'config.json.example',
'epochs': 1,
'timerange': None,
'spaces': 'all',
'hyperopt_jobs': 1,
'hyperopt_continue': True
})
mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
Hyperopt(default_conf)
assert unlinkmock.call_count == 0
assert log_has(f"Continuing on previous hyperopt results.", caplog)
def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
)
patch_exchange(mocker)
default_conf.update({'config': 'config.json.example',
'epochs': 1,
'timerange': None,
'spaces': 'all',
'hyperopt_jobs': 1,
'print_json': True,
})
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start()
parallel.assert_called_once()
out, err = capsys.readouterr()
assert '{"params":{"mfi-value":null,"fastd-value":null,"adx-value":null,"rsi-value":null,"mfi-enabled":null,"fastd-enabled":null,"adx-enabled":null,"rsi-enabled":null,"trigger":null,"sell-mfi-value":null,"sell-fastd-value":null,"sell-adx-value":null,"sell-rsi-value":null,"sell-mfi-enabled":null,"sell-fastd-enabled":null,"sell-adx-enabled":null,"sell-rsi-enabled":null,"sell-trigger":null},"minimal_roi":{},"stoploss":null}' in out # noqa: E501
assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations
assert dumper.call_count == 2
def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> None:
dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch(
'freqtrade.optimize.hyperopt.get_timeframe',
MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13)))
)
parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}])
)
patch_exchange(mocker)
default_conf.update({'config': 'config.json.example',
'epochs': 1,
'timerange': None,
'spaces': 'roi stoploss',
'hyperopt_jobs': 1,
'print_json': True,
})
hyperopt = Hyperopt(default_conf)
hyperopt.backtesting.strategy.tickerdata_to_dataframe = MagicMock()
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
hyperopt.start()
parallel.assert_called_once()
out, err = capsys.readouterr()
assert '{"minimal_roi":{},"stoploss":null}' in out
assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations
assert dumper.call_count == 2

View File

@@ -34,9 +34,9 @@ def whitelist_conf(default_conf):
def test_load_pairlist_noexist(mocker, markets, default_conf): def test_load_pairlist_noexist(mocker, markets, default_conf):
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
with pytest.raises(ImportError, with pytest.raises(OperationalException,
match=r"Impossible to load Pairlist 'NonexistingPairList'." match=r"Impossible to load Pairlist 'NonexistingPairList'. "
r" This class does not exist or contains Python code errors"): r"This class does not exist or contains Python code errors."):
PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist

View File

@@ -91,7 +91,7 @@ def test_fiat_convert_unsupported_crypto(mocker, caplog):
mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[]) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0 assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples) assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog)
def test_fiat_convert_get_price(mocker): def test_fiat_convert_get_price(mocker):
@@ -190,7 +190,7 @@ def test_fiat_invalid_response(mocker, caplog):
length_cryptomap = len(fiat_convert._cryptomap) length_cryptomap = len(fiat_convert._cryptomap)
assert length_cryptomap == 0 assert length_cryptomap == 0
assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError', assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError',
caplog.record_tuples) caplog)
def test_convert_amount(mocker): def test_convert_amount(mocker):

View File

@@ -44,7 +44,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
with pytest.raises(RPCException, match=r'.*no active trade*'): with pytest.raises(RPCException, match=r'.*no active trade*'):
rpc._rpc_trade_status() rpc._rpc_trade_status()
freqtradebot.create_trade() freqtradebot.create_trades()
results = rpc._rpc_trade_status() results = rpc._rpc_trade_status()
assert { assert {
'trade_id': 1, 'trade_id': 1,
@@ -116,7 +116,7 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
with pytest.raises(RPCException, match=r'.*no active order*'): with pytest.raises(RPCException, match=r'.*no active order*'):
rpc._rpc_status_table() rpc._rpc_status_table()
freqtradebot.create_trade() freqtradebot.create_trades()
result = rpc._rpc_status_table() result = rpc._rpc_status_table()
assert 'instantly' in result['Since'].all() assert 'instantly' in result['Since'].all()
assert 'ETH/BTC' in result['Pair'].all() assert 'ETH/BTC' in result['Pair'].all()
@@ -151,7 +151,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter() rpc._fiat_converter = CryptoToFiatConverter()
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@@ -208,7 +208,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
@@ -222,7 +222,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
@@ -292,7 +292,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
@@ -324,7 +324,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
assert prec_satoshi(stats['best_rate'], 6.2) assert prec_satoshi(stats['best_rate'], 6.2)
def test_rpc_balance_handle(default_conf, mocker): def test_rpc_balance_handle_error(default_conf, mocker):
mock_balance = { mock_balance = {
'BTC': { 'BTC': {
'free': 10.0, 'free': 10.0,
@@ -371,6 +371,72 @@ def test_rpc_balance_handle(default_conf, mocker):
assert result['total'] == 12.0 assert result['total'] == 12.0
def test_rpc_balance_handle(default_conf, mocker):
mock_balance = {
'BTC': {
'free': 10.0,
'total': 12.0,
'used': 2.0,
},
'ETH': {
'free': 1.0,
'total': 5.0,
'used': 4.0,
},
'PAX': {
'free': 5.0,
'total': 10.0,
'used': 5.0,
}
}
mocker.patch.multiple(
'freqtrade.rpc.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
get_balances=MagicMock(return_value=mock_balance),
get_ticker=MagicMock(
side_effect=lambda p, r: {'bid': 100} if p == "BTC/PAX" else {'bid': 0.01}),
get_valid_pair_combination=MagicMock(
side_effect=lambda a, b: f"{b}/{a}" if a == "PAX" else f"{a}/{b}")
)
freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
result = rpc._rpc_balance(default_conf['fiat_display_currency'])
assert prec_satoshi(result['total'], 12.15)
assert prec_satoshi(result['value'], 182250)
assert 'USD' == result['symbol']
assert result['currencies'] == [
{'currency': 'BTC',
'available': 10.0,
'balance': 12.0,
'pending': 2.0,
'est_btc': 12.0,
},
{'available': 1.0,
'balance': 5.0,
'currency': 'ETH',
'est_btc': 0.05,
'pending': 4.0
},
{'available': 5.0,
'balance': 10.0,
'currency': 'PAX',
'est_btc': 0.1,
'pending': 5.0}
]
assert result['total'] == 12.15
def test_rpc_start(mocker, default_conf) -> None: def test_rpc_start(mocker, default_conf) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
@@ -470,7 +536,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
msg = rpc._rpc_forcesell('all') msg = rpc._rpc_forcesell('all')
assert msg == {'result': 'Created sell orders for all open trades.'} assert msg == {'result': 'Created sell orders for all open trades.'}
freqtradebot.create_trade() freqtradebot.create_trades()
msg = rpc._rpc_forcesell('all') msg = rpc._rpc_forcesell('all')
assert msg == {'result': 'Created sell orders for all open trades.'} assert msg == {'result': 'Created sell orders for all open trades.'}
@@ -504,7 +570,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount assert trade.amount == filled_amount
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.filter(Trade.id == '2').first() trade = Trade.query.filter(Trade.id == '2').first()
amount = trade.amount amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it # make an limit-buy open trade, if there is no 'filled', don't sell it
@@ -523,7 +589,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
assert cancel_order_mock.call_count == 2 assert cancel_order_mock.call_count == 2
assert trade.amount == amount assert trade.amount == amount
freqtradebot.create_trade() freqtradebot.create_trades()
# make an limit-sell open trade # make an limit-sell open trade
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.get_order', 'freqtrade.exchange.Exchange.get_order',
@@ -556,7 +622,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@@ -594,7 +660,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
assert counts["current"] == 0 assert counts["current"] == 0
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
counts = rpc._rpc_count() counts = rpc._rpc_count()
assert counts["current"] == 1 assert counts["current"] == 1

View File

@@ -148,8 +148,8 @@ def test_api_run(default_conf, mocker, caplog):
assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert isinstance(server_mock.call_args_list[0][0][2], Flask)
assert hasattr(apiserver, "srv") assert hasattr(apiserver, "srv")
assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog.record_tuples) assert log_has("Starting HTTP Server at 127.0.0.1:8080", caplog)
assert log_has("Starting Local Rest Server.", caplog.record_tuples) assert log_has("Starting Local Rest Server.", caplog)
# Test binding to public # Test binding to public
caplog.clear() caplog.clear()
@@ -165,22 +165,20 @@ def test_api_run(default_conf, mocker, caplog):
assert server_mock.call_args_list[0][0][0] == "0.0.0.0" assert server_mock.call_args_list[0][0][0] == "0.0.0.0"
assert server_mock.call_args_list[0][0][1] == "8089" assert server_mock.call_args_list[0][0][1] == "8089"
assert isinstance(server_mock.call_args_list[0][0][2], Flask) assert isinstance(server_mock.call_args_list[0][0][2], Flask)
assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog.record_tuples) assert log_has("Starting HTTP Server at 0.0.0.0:8089", caplog)
assert log_has("Starting Local Rest Server.", caplog.record_tuples) assert log_has("Starting Local Rest Server.", caplog)
assert log_has("SECURITY WARNING - Local Rest Server listening to external connections", assert log_has("SECURITY WARNING - Local Rest Server listening to external connections",
caplog.record_tuples) caplog)
assert log_has("SECURITY WARNING - This is insecure please set to your loopback," assert log_has("SECURITY WARNING - This is insecure please set to your loopback,"
"e.g 127.0.0.1 in config.json", "e.g 127.0.0.1 in config.json", caplog)
caplog.record_tuples)
assert log_has("SECURITY WARNING - No password for local REST Server defined. " assert log_has("SECURITY WARNING - No password for local REST Server defined. "
"Please make sure that this is intentional!", "Please make sure that this is intentional!", caplog)
caplog.record_tuples)
# Test crashing flask # Test crashing flask
caplog.clear() caplog.clear()
mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception)) mocker.patch('freqtrade.rpc.api_server.make_server', MagicMock(side_effect=Exception))
apiserver.run() apiserver.run()
assert log_has("Api server failed to start.", caplog.record_tuples) assert log_has("Api server failed to start.", caplog)
def test_api_cleanup(default_conf, mocker, caplog): def test_api_cleanup(default_conf, mocker, caplog):
@@ -199,7 +197,7 @@ def test_api_cleanup(default_conf, mocker, caplog):
apiserver.cleanup() apiserver.cleanup()
assert stop_mock.shutdown.call_count == 1 assert stop_mock.shutdown.call_count == 1
assert log_has("Stopping API Server", caplog.record_tuples) assert log_has("Stopping API Server", caplog)
def test_api_reloadconf(botclient): def test_api_reloadconf(botclient):
@@ -244,6 +242,8 @@ def test_api_balance(botclient, mocker, rpc_balance):
} }
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
side_effect=lambda a, b: f"{a}/{b}")
rc = client_get(client, f"{BASE_URI}/balance") rc = client_get(client, f"{BASE_URI}/balance")
assert_response(rc) assert_response(rc)
@@ -275,7 +275,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets):
assert rc.json["max"] == 1.0 assert rc.json["max"] == 1.0
# Create some test data # Create some test data
ftbot.create_trade() ftbot.create_trades()
rc = client_get(client, f"{BASE_URI}/count") rc = client_get(client, f"{BASE_URI}/count")
assert_response(rc) assert_response(rc)
assert rc.json["current"] == 1.0 assert rc.json["current"] == 1.0
@@ -329,7 +329,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
assert len(rc.json) == 1 assert len(rc.json) == 1
assert rc.json == {"error": "Error querying _profit: no closed trade"} assert rc.json == {"error": "Error querying _profit: no closed trade"}
ftbot.create_trade() ftbot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
@@ -418,7 +418,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json == {'error': 'Error querying _status: no active trade'} assert rc.json == {'error': 'Error querying _status: no active trade'}
ftbot.create_trade() ftbot.create_trades()
rc = client_get(client, f"{BASE_URI}/status") rc = client_get(client, f"{BASE_URI}/status")
assert_response(rc) assert_response(rc)
assert len(rc.json) == 1 assert len(rc.json) == 1
@@ -548,7 +548,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
assert_response(rc, 502) assert_response(rc, 502)
assert rc.json == {"error": "Error querying _forcesell: invalid argument"} assert rc.json == {"error": "Error querying _forcesell: invalid argument"}
ftbot.create_trade() ftbot.create_trades()
rc = client_post(client, f"{BASE_URI}/forcesell", rc = client_post(client, f"{BASE_URI}/forcesell",
data='{"tradeid": "1"}') data='{"tradeid": "1"}')

View File

@@ -19,7 +19,7 @@ def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert not log_has('Enabling rpc.telegram ...', caplog)
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
@@ -28,7 +28,7 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert log_has('Enabling rpc.telegram ...', caplog)
len_modules = len(rpc_manager.registered_modules) len_modules = len(rpc_manager.registered_modules)
assert len_modules == 1 assert len_modules == 1
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules] assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
@@ -43,7 +43,7 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.cleanup() rpc_manager.cleanup()
assert not log_has('Cleaning up rpc.telegram ...', caplog.record_tuples) assert not log_has('Cleaning up rpc.telegram ...', caplog)
assert telegram_mock.call_count == 0 assert telegram_mock.call_count == 0
@@ -59,7 +59,7 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules] assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
rpc_manager.cleanup() rpc_manager.cleanup()
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples) assert log_has('Cleaning up rpc.telegram ...', caplog)
assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules] assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1
@@ -75,7 +75,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
'status': 'test' 'status': 'test'
}) })
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog)
assert telegram_mock.call_count == 0 assert telegram_mock.call_count == 0
@@ -90,7 +90,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
'status': 'test' 'status': 'test'
}) })
assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog)
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1
@@ -100,7 +100,7 @@ def test_init_webhook_disabled(mocker, default_conf, caplog) -> None:
default_conf['webhook'] = {'enabled': False} default_conf['webhook'] = {'enabled': False}
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples) assert not log_has('Enabling rpc.webhook ...', caplog)
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
@@ -110,7 +110,7 @@ def test_init_webhook_enabled(mocker, default_conf, caplog) -> None:
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"} default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert log_has('Enabling rpc.webhook ...', caplog.record_tuples) assert log_has('Enabling rpc.webhook ...', caplog)
assert len(rpc_manager.registered_modules) == 1 assert len(rpc_manager.registered_modules) == 1
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
@@ -144,7 +144,7 @@ def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None:
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert not log_has('Enabling rpc.api_server', caplog.record_tuples) assert not log_has('Enabling rpc.api_server', caplog)
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
assert run_mock.call_count == 0 assert run_mock.call_count == 0
@@ -160,7 +160,7 @@ def test_init_apiserver_enabled(mocker, default_conf, caplog) -> None:
"listen_port": "8080"} "listen_port": "8080"}
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert log_has('Enabling rpc.api_server', caplog.record_tuples) assert log_has('Enabling rpc.api_server', caplog)
assert len(rpc_manager.registered_modules) == 1 assert len(rpc_manager.registered_modules) == 1
assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules] assert 'apiserver' in [mod.name for mod in rpc_manager.registered_modules]
assert run_mock.call_count == 1 assert run_mock.call_count == 1

View File

@@ -76,7 +76,7 @@ def test_init(default_conf, mocker, caplog) -> None:
"['performance'], ['daily'], ['count'], ['reload_conf'], " \ "['performance'], ['daily'], ['count'], ['reload_conf'], " \
"['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]" "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]"
assert log_has(message_str, caplog.record_tuples) assert log_has(message_str, caplog)
def test_cleanup(default_conf, mocker) -> None: def test_cleanup(default_conf, mocker) -> None:
@@ -102,18 +102,9 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
dummy = DummyCls(bot) dummy = DummyCls(bot)
dummy.dummy_handler(bot=MagicMock(), update=update) dummy.dummy_handler(bot=MagicMock(), update=update)
assert dummy.state['called'] is True assert dummy.state['called'] is True
assert log_has( assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
'Executing handler: dummy_handler for chat_id: 0', assert not log_has('Rejected unauthorized message from: 0', caplog)
caplog.record_tuples assert not log_has('Exception occurred within Telegram module', caplog)
)
assert not log_has(
'Rejected unauthorized message from: 0',
caplog.record_tuples
)
assert not log_has(
'Exception occurred within Telegram module',
caplog.record_tuples
)
def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
@@ -128,18 +119,9 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
dummy = DummyCls(bot) dummy = DummyCls(bot)
dummy.dummy_handler(bot=MagicMock(), update=update) dummy.dummy_handler(bot=MagicMock(), update=update)
assert dummy.state['called'] is False assert dummy.state['called'] is False
assert not log_has( assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog)
'Executing handler: dummy_handler for chat_id: 3735928559', assert log_has('Rejected unauthorized message from: 3735928559', caplog)
caplog.record_tuples assert not log_has('Exception occurred within Telegram module', caplog)
)
assert log_has(
'Rejected unauthorized message from: 3735928559',
caplog.record_tuples
)
assert not log_has(
'Exception occurred within Telegram module',
caplog.record_tuples
)
def test_authorized_only_exception(default_conf, mocker, caplog) -> None: def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
@@ -156,18 +138,9 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
dummy.dummy_exception(bot=MagicMock(), update=update) dummy.dummy_exception(bot=MagicMock(), update=update)
assert dummy.state['called'] is False assert dummy.state['called'] is False
assert not log_has( assert not log_has('Executing handler: dummy_handler for chat_id: 0', caplog)
'Executing handler: dummy_handler for chat_id: 0', assert not log_has('Rejected unauthorized message from: 0', caplog)
caplog.record_tuples assert log_has('Exception occurred within Telegram module', caplog)
)
assert not log_has(
'Rejected unauthorized message from: 0',
caplog.record_tuples
)
assert log_has(
'Exception occurred within Telegram module',
caplog.record_tuples
)
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
@@ -219,7 +192,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
# Create some test data # Create some test data
for _ in range(3): for _ in range(3):
freqtradebot.create_trade() freqtradebot.create_trades()
telegram._status(bot=MagicMock(), update=update) telegram._status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@@ -267,7 +240,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
# Trigger status while we have a fulfilled order for the open trade # Trigger status while we have a fulfilled order for the open trade
telegram._status(bot=MagicMock(), update=update) telegram._status(bot=MagicMock(), update=update)
@@ -319,7 +292,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
telegram._status_table(bot=MagicMock(), update=update) telegram._status_table(bot=MagicMock(), update=update)
@@ -335,6 +308,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
limit_sell_order, markets, mocker) -> None: limit_sell_order, markets, mocker) -> None:
patch_exchange(mocker) patch_exchange(mocker)
default_conf['max_open_trades'] = 1
mocker.patch( mocker.patch(
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
return_value=15000.0 return_value=15000.0
@@ -358,7 +332,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@@ -384,9 +358,9 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
# Reset msg_mock # Reset msg_mock
msg_mock.reset_mock() msg_mock.reset_mock()
freqtradebot.config['max_open_trades'] = 2
# Add two other trades # Add two other trades
freqtradebot.create_trade() freqtradebot.create_trades()
freqtradebot.create_trade()
trades = Trade.query.all() trades = Trade.query.all()
for trade in trades: for trade in trades:
@@ -465,7 +439,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
@@ -518,6 +492,8 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance) -> N
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
side_effect=lambda a, b: f"{a}/{b}")
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@@ -559,10 +535,32 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.config['dry_run'] = False
telegram._balance(bot=MagicMock(), update=update) telegram._balance(bot=MagicMock(), update=update)
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'all balances are zero' in result assert 'All balances are zero.' in result
def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update)
result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1
assert "Running in Dry Run, balances are not available." in result
def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: def test_balance_handle_too_large_response(default_conf, update, mocker) -> None:
@@ -736,7 +734,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@@ -787,7 +785,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
# Decrease the price and sell it # Decrease the price and sell it
mocker.patch.multiple( mocker.patch.multiple(
@@ -835,14 +833,13 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
markets=PropertyMock(return_value=markets), markets=PropertyMock(return_value=markets),
validate_pairs=MagicMock(return_value={}) validate_pairs=MagicMock(return_value={})
) )
default_conf['max_open_trades'] = 4
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False)) patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
for _ in range(4): freqtradebot.create_trades()
freqtradebot.create_trade()
rpc_mock.reset_mock() rpc_mock.reset_mock()
update.message.text = '/forcesell all' update.message.text = '/forcesell all'
@@ -986,7 +983,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@@ -1031,7 +1028,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trades()
msg_mock.reset_mock() msg_mock.reset_mock()
telegram._count(bot=MagicMock(), update=update) telegram._count(bot=MagicMock(), update=update)
@@ -1416,7 +1413,4 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
# Bot should've tried to send it twice # Bot should've tried to send it twice
assert len(bot.method_calls) == 2 assert len(bot.method_calls) == 2
assert log_has( assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog)
'Telegram NetworkError: Oh snap! Trying one more time.',
caplog.record_tuples
)

View File

@@ -115,7 +115,7 @@ def test_exception_send_msg(default_conf, mocker, caplog):
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks", assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks",
caplog.record_tuples) caplog)
default_conf["webhook"] = get_webhook_dict() default_conf["webhook"] = get_webhook_dict()
default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}" default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}"
@@ -135,7 +135,7 @@ def test_exception_send_msg(default_conf, mocker, caplog):
} }
webhook.send_msg(msg) webhook.send_msg(msg)
assert log_has("Problem calling Webhook. Please check your webhook configuration. " assert log_has("Problem calling Webhook. Please check your webhook configuration. "
"Exception: 'DEADBEEF'", caplog.record_tuples) "Exception: 'DEADBEEF'", caplog)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
@@ -164,4 +164,4 @@ def test__send_msg(default_conf, mocker, caplog):
post = MagicMock(side_effect=RequestException) post = MagicMock(side_effect=RequestException)
mocker.patch("freqtrade.rpc.webhook.post", post) mocker.patch("freqtrade.rpc.webhook.post", post)
webhook._send_msg(msg) webhook._send_msg(msg)
assert log_has('Could not call webhook url. Exception: ', caplog.record_tuples) assert log_has('Could not call webhook url. Exception: ', caplog)

View File

@@ -6,7 +6,7 @@ from unittest.mock import MagicMock
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.arguments import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.data.history import load_tickerdata_file from freqtrade.data.history import load_tickerdata_file
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@@ -19,13 +19,13 @@ _STRATEGY = DefaultStrategy(config={})
def test_returns_latest_buy_signal(mocker, default_conf, ticker_history): def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'analyze_ticker', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
) )
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'analyze_ticker', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
) )
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
@@ -33,14 +33,14 @@ def test_returns_latest_buy_signal(mocker, default_conf, ticker_history):
def test_returns_latest_sell_signal(mocker, default_conf, ticker_history): def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'analyze_ticker', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
) )
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (False, True)
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'analyze_ticker', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
) )
assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False) assert _STRATEGY.get_signal('ETH/BTC', '5m', ticker_history) == (True, False)
@@ -49,34 +49,34 @@ def test_returns_latest_sell_signal(mocker, default_conf, ticker_history):
def test_get_signal_empty(default_conf, mocker, caplog): def test_get_signal_empty(default_conf, mocker, caplog):
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
DataFrame()) DataFrame())
assert log_has('Empty ticker history for pair foo', caplog.record_tuples) assert log_has('Empty ticker history for pair foo', caplog)
caplog.clear() caplog.clear()
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'],
[]) [])
assert log_has('Empty ticker history for pair bar', caplog.record_tuples) assert log_has('Empty ticker history for pair bar', caplog)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history): def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ticker_history):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'analyze_ticker', _STRATEGY, '_analyze_ticker_internal',
side_effect=ValueError('xyz') side_effect=ValueError('xyz')
) )
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
ticker_history) ticker_history)
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples) assert log_has('Unable to analyze ticker for pair foo: xyz', caplog)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history): def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'analyze_ticker', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([]) return_value=DataFrame([])
) )
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ticker_history) ticker_history)
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples) assert log_has('Empty dataframe for pair xyz', caplog)
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history):
@@ -86,15 +86,12 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history):
oldtime = arrow.utcnow().shift(minutes=-16) oldtime = arrow.utcnow().shift(minutes=-16)
ticks = DataFrame([{'buy': 1, 'date': oldtime}]) ticks = DataFrame([{'buy': 1, 'date': oldtime}])
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'analyze_ticker', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame(ticks) return_value=DataFrame(ticks)
) )
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
ticker_history) ticker_history)
assert log_has( assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
'Outdated history for pair xyz. Last tick is 16 minutes old',
caplog.record_tuples
)
def test_get_signal_handles_exceptions(mocker, default_conf): def test_get_signal_handles_exceptions(mocker, default_conf):
@@ -186,6 +183,39 @@ def test_min_roi_reached2(default_conf, fee) -> None:
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
def test_min_roi_reached3(default_conf, fee) -> None:
# test for issue #1948
min_roi = {20: 0.07,
30: 0.05,
55: 0.30,
}
strategy = DefaultStrategy(default_conf)
strategy.minimal_roi = min_roi
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
)
assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime)
assert not strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime)
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime)
# Should not trigger with 20% profit since after 55 minutes only 30% is active.
assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime)
assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime)
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)
@@ -204,9 +234,8 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert log_has('TA Analysis Launched', caplog.record_tuples) assert log_has('TA Analysis Launched', caplog)
assert not log_has('Skipping TA Analysis for already analyzed candle', assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.record_tuples)
caplog.clear() caplog.clear()
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
@@ -214,12 +243,11 @@ def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
assert ind_mock.call_count == 2 assert ind_mock.call_count == 2
assert buy_mock.call_count == 2 assert buy_mock.call_count == 2
assert buy_mock.call_count == 2 assert buy_mock.call_count == 2
assert log_has('TA Analysis Launched', caplog.record_tuples) assert log_has('TA Analysis Launched', caplog)
assert not log_has('Skipping TA Analysis for already analyzed candle', assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.record_tuples)
def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None: def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x) ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x) buy_mock = MagicMock(side_effect=lambda x, meta: x)
@@ -234,7 +262,7 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
strategy = DefaultStrategy({}) strategy = DefaultStrategy({})
strategy.process_only_new_candles = True strategy.process_only_new_candles = True
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
assert 'high' in ret.columns assert 'high' in ret.columns
assert 'low' in ret.columns assert 'low' in ret.columns
assert 'close' in ret.columns assert 'close' in ret.columns
@@ -242,12 +270,11 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert log_has('TA Analysis Launched', caplog.record_tuples) assert log_has('TA Analysis Launched', caplog)
assert not log_has('Skipping TA Analysis for already analyzed candle', assert not log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.record_tuples)
caplog.clear() caplog.clear()
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'}) ret = strategy._analyze_ticker_internal(ticker_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true # No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 1 assert ind_mock.call_count == 1
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
@@ -257,6 +284,21 @@ def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
assert 'sell' in ret.columns assert 'sell' in ret.columns
assert ret['buy'].sum() == 0 assert ret['buy'].sum() == 0
assert ret['sell'].sum() == 0 assert ret['sell'].sum() == 0
assert not log_has('TA Analysis Launched', caplog.record_tuples) assert not log_has('TA Analysis Launched', caplog)
assert log_has('Skipping TA Analysis for already analyzed candle', assert log_has('Skipping TA Analysis for already analyzed candle', caplog)
caplog.record_tuples)
def test_is_pair_locked(default_conf):
strategy = DefaultStrategy(default_conf)
# dict should be empty
assert not strategy._pair_locked_until
pair = 'ETH/BTC'
assert not strategy.is_pair_locked(pair)
strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime)
# ETH/BTC locked for 4 minutes
assert strategy.is_pair_locked(pair)
# XRP/BTC should not be locked now
pair = 'XRP/BTC'
assert not strategy.is_pair_locked(pair)

View File

@@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103 # pragma pylint: disable=missing-docstring, protected-access, C0103
import logging import logging
import tempfile
import warnings import warnings
from base64 import urlsafe_b64encode from base64 import urlsafe_b64encode
from os import path from os import path
@@ -9,11 +10,12 @@ from unittest.mock import Mock
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade import OperationalException
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy import import_strategy from freqtrade.strategy import import_strategy
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
from freqtrade.tests.conftest import log_has_re from freqtrade.tests.conftest import log_has, log_has_re
def test_import_strategy(caplog): def test_import_strategy(caplog):
@@ -33,87 +35,90 @@ def test_import_strategy(caplog):
assert imported_strategy.__module__ == 'freqtrade.strategy' assert imported_strategy.__module__ == 'freqtrade.strategy'
assert imported_strategy.some_method() == 42 assert imported_strategy.some_method() == 42
assert ( assert log_has('Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy '
'freqtrade.strategy', 'as freqtrade.strategy.DefaultStrategy', caplog)
logging.DEBUG,
'Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy '
'as freqtrade.strategy.DefaultStrategy',
) in caplog.record_tuples
def test_search_strategy(): def test_search_strategy():
default_config = {} default_config = {}
default_location = Path(__file__).parent.parent.joinpath('strategy').resolve() default_location = Path(__file__).parent.parent.parent.joinpath('strategy').resolve()
assert isinstance(
StrategyResolver._search_object( s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_type=IStrategy, object_type=IStrategy,
kwargs={'config': default_config}, kwargs={'config': default_config},
object_name='DefaultStrategy' object_name='DefaultStrategy'
),
IStrategy
) )
assert StrategyResolver._search_object( assert isinstance(s, IStrategy)
s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_type=IStrategy, object_type=IStrategy,
kwargs={'config': default_config}, kwargs={'config': default_config},
object_name='NotFoundStrategy' object_name='NotFoundStrategy'
) is None )
assert s is None
def test_load_strategy(result): def test_load_strategy(default_conf, result):
resolver = StrategyResolver({'strategy': 'TestStrategy'}) default_conf.update({'strategy': 'TestStrategy'})
resolver = StrategyResolver(default_conf)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_strategy_byte64(result): def test_load_strategy_base64(result, caplog, default_conf):
with open("freqtrade/tests/strategy/test_strategy.py", "r") as file: with open("user_data/strategies/test_strategy.py", "rb") as file:
encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8") encoded_string = urlsafe_b64encode(file.read()).decode("utf-8")
resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) default_conf.update({'strategy': 'TestStrategy:{}'.format(encoded_string)})
resolver = StrategyResolver(default_conf)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
# Make sure strategy was loaded from base64 (using temp directory)!!
assert log_has_re(r"Using resolved strategy TestStrategy from '"
+ tempfile.gettempdir() + r"/.*/TestStrategy\.py'\.\.\.", caplog)
def test_load_strategy_invalid_directory(result, caplog): def test_load_strategy_invalid_directory(result, caplog, default_conf):
resolver = StrategyResolver() resolver = StrategyResolver(default_conf)
extra_dir = Path.cwd() / 'some/path' extra_dir = Path.cwd() / 'some/path'
resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) resolver._load_strategy('TestStrategy', config=default_conf, extra_dir=extra_dir)
assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog.record_tuples) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog)
assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
def test_load_not_found_strategy(): def test_load_not_found_strategy(default_conf):
strategy = StrategyResolver() strategy = StrategyResolver(default_conf)
with pytest.raises(ImportError, with pytest.raises(OperationalException,
match=r"Impossible to load Strategy 'NotFoundStrategy'." match=r"Impossible to load Strategy 'NotFoundStrategy'. "
r" This class does not exist or contains Python code errors"): r"This class does not exist or contains Python code errors."):
strategy._load_strategy(strategy_name='NotFoundStrategy', config={}) strategy._load_strategy(strategy_name='NotFoundStrategy', config=default_conf)
def test_load_staticmethod_importerror(mocker, caplog): def test_load_staticmethod_importerror(mocker, caplog, default_conf):
mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock( mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock(
side_effect=TypeError("can't pickle staticmethod objects"))) side_effect=TypeError("can't pickle staticmethod objects")))
with pytest.raises(ImportError, with pytest.raises(OperationalException,
match=r"Impossible to load Strategy 'DefaultStrategy'." match=r"Impossible to load Strategy 'DefaultStrategy'. "
r" This class does not exist or contains Python code errors"): r"This class does not exist or contains Python code errors."):
StrategyResolver() StrategyResolver(default_conf)
assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples) assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog)
def test_strategy(result): def test_strategy(result, default_conf):
config = {'strategy': 'DefaultStrategy'} default_conf.update({'strategy': 'DefaultStrategy'})
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
assert resolver.strategy.minimal_roi[0] == 0.04 assert resolver.strategy.minimal_roi[0] == 0.04
assert config["minimal_roi"]['0'] == 0.04 assert default_conf["minimal_roi"]['0'] == 0.04
assert resolver.strategy.stoploss == -0.10 assert resolver.strategy.stoploss == -0.10
assert config['stoploss'] == -0.10 assert default_conf['stoploss'] == -0.10
assert resolver.strategy.ticker_interval == '5m' assert resolver.strategy.ticker_interval == '5m'
assert config['ticker_interval'] == '5m' assert default_conf['ticker_interval'] == '5m'
df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata) df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata)
assert 'adx' in df_indicators assert 'adx' in df_indicators
@@ -125,112 +130,95 @@ def test_strategy(result):
assert 'sell' in dataframe.columns assert 'sell' in dataframe.columns
def test_strategy_override_minimal_roi(caplog): def test_strategy_override_minimal_roi(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'minimal_roi': { 'minimal_roi': {
"0": 0.5 "0": 0.5
} }
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.minimal_roi[0] == 0.5 assert resolver.strategy.minimal_roi[0] == 0.5
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'minimal_roi' with value in config file: {'0': 0.5}.", caplog)
logging.INFO,
"Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
) in caplog.record_tuples
def test_strategy_override_stoploss(caplog): def test_strategy_override_stoploss(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'stoploss': -0.5 'stoploss': -0.5
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.stoploss == -0.5 assert resolver.strategy.stoploss == -0.5
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog)
logging.INFO,
"Override strategy 'stoploss' with value in config file: -0.5."
) in caplog.record_tuples
def test_strategy_override_trailing_stop(caplog): def test_strategy_override_trailing_stop(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'trailing_stop': True 'trailing_stop': True
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.trailing_stop assert resolver.strategy.trailing_stop
assert isinstance(resolver.strategy.trailing_stop, bool) assert isinstance(resolver.strategy.trailing_stop, bool)
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'trailing_stop' with value in config file: True.", caplog)
logging.INFO,
"Override strategy 'trailing_stop' with value in config file: True."
) in caplog.record_tuples
def test_strategy_override_trailing_stop_positive(caplog): def test_strategy_override_trailing_stop_positive(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'trailing_stop_positive': -0.1, 'trailing_stop_positive': -0.1,
'trailing_stop_positive_offset': -0.2 'trailing_stop_positive_offset': -0.2
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.trailing_stop_positive == -0.1 assert resolver.strategy.trailing_stop_positive == -0.1
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.",
logging.INFO, caplog)
"Override strategy 'trailing_stop_positive' with value in config file: -0.1."
) in caplog.record_tuples
assert resolver.strategy.trailing_stop_positive_offset == -0.2 assert resolver.strategy.trailing_stop_positive_offset == -0.2
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.",
logging.INFO, caplog)
"Override strategy 'trailing_stop_positive' with value in config file: -0.1."
) in caplog.record_tuples
def test_strategy_override_ticker_interval(caplog): def test_strategy_override_ticker_interval(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'ticker_interval': 60, 'ticker_interval': 60,
'stake_currency': 'ETH' 'stake_currency': 'ETH'
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.ticker_interval == 60 assert resolver.strategy.ticker_interval == 60
assert resolver.strategy.stake_currency == 'ETH' assert resolver.strategy.stake_currency == 'ETH'
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'ticker_interval' with value in config file: 60.",
logging.INFO, caplog)
"Override strategy 'ticker_interval' with value in config file: 60."
) in caplog.record_tuples
def test_strategy_override_process_only_new_candles(caplog): def test_strategy_override_process_only_new_candles(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'process_only_new_candles': True 'process_only_new_candles': True
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.process_only_new_candles assert resolver.strategy.process_only_new_candles
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'process_only_new_candles' with value in config file: True.",
logging.INFO, caplog)
"Override strategy 'process_only_new_candles' with value in config file: True."
) in caplog.record_tuples
def test_strategy_override_order_types(caplog): def test_strategy_override_order_types(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
order_types = { order_types = {
@@ -239,36 +227,32 @@ def test_strategy_override_order_types(caplog):
'stoploss': 'limit', 'stoploss': 'limit',
'stoploss_on_exchange': True, 'stoploss_on_exchange': True,
} }
default_conf.update({
config = {
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'order_types': order_types 'order_types': order_types
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.order_types assert resolver.strategy.order_types
for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']:
assert resolver.strategy.order_types[method] == order_types[method] assert resolver.strategy.order_types[method] == order_types[method]
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'order_types' with value in config file:"
logging.INFO, " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit',"
"Override strategy 'order_types' with value in config file:" " 'stoploss_on_exchange': True}.", caplog)
" {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit',"
" 'stoploss_on_exchange': True}."
) in caplog.record_tuples
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'order_types': {'buy': 'market'} 'order_types': {'buy': 'market'}
} })
# Raise error for invalid configuration # Raise error for invalid configuration
with pytest.raises(ImportError, with pytest.raises(ImportError,
match=r"Impossible to load Strategy 'DefaultStrategy'. " match=r"Impossible to load Strategy 'DefaultStrategy'. "
r"Order-types mapping is incomplete."): r"Order-types mapping is incomplete."):
StrategyResolver(config) StrategyResolver(default_conf)
def test_strategy_override_order_tif(caplog): def test_strategy_override_order_tif(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
order_time_in_force = { order_time_in_force = {
@@ -276,93 +260,86 @@ def test_strategy_override_order_tif(caplog):
'sell': 'gtc', 'sell': 'gtc',
} }
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'order_time_in_force': order_time_in_force 'order_time_in_force': order_time_in_force
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.order_time_in_force assert resolver.strategy.order_time_in_force
for method in ['buy', 'sell']: for method in ['buy', 'sell']:
assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method] assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method]
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'order_time_in_force' with value in config file:"
logging.INFO, " {'buy': 'fok', 'sell': 'gtc'}.", caplog)
"Override strategy 'order_time_in_force' with value in config file:"
" {'buy': 'fok', 'sell': 'gtc'}."
) in caplog.record_tuples
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'order_time_in_force': {'buy': 'fok'} 'order_time_in_force': {'buy': 'fok'}
} })
# Raise error for invalid configuration # Raise error for invalid configuration
with pytest.raises(ImportError, with pytest.raises(ImportError,
match=r"Impossible to load Strategy 'DefaultStrategy'. " match=r"Impossible to load Strategy 'DefaultStrategy'. "
r"Order-time-in-force mapping is incomplete."): r"Order-time-in-force mapping is incomplete."):
StrategyResolver(config) StrategyResolver(default_conf)
def test_strategy_override_use_sell_signal(caplog): def test_strategy_override_use_sell_signal(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert not resolver.strategy.use_sell_signal assert not resolver.strategy.use_sell_signal
assert isinstance(resolver.strategy.use_sell_signal, bool) assert isinstance(resolver.strategy.use_sell_signal, bool)
# must be inserted to configuration # must be inserted to configuration
assert 'use_sell_signal' in config['experimental'] assert 'use_sell_signal' in default_conf['experimental']
assert not config['experimental']['use_sell_signal'] assert not default_conf['experimental']['use_sell_signal']
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'experimental': { 'experimental': {
'use_sell_signal': True, 'use_sell_signal': True,
}, },
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.use_sell_signal assert resolver.strategy.use_sell_signal
assert isinstance(resolver.strategy.use_sell_signal, bool) assert isinstance(resolver.strategy.use_sell_signal, bool)
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'use_sell_signal' with value in config file: True.", caplog)
logging.INFO,
"Override strategy 'use_sell_signal' with value in config file: True."
) in caplog.record_tuples
def test_strategy_override_use_sell_profit_only(caplog): def test_strategy_override_use_sell_profit_only(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert not resolver.strategy.sell_profit_only assert not resolver.strategy.sell_profit_only
assert isinstance(resolver.strategy.sell_profit_only, bool) assert isinstance(resolver.strategy.sell_profit_only, bool)
# must be inserted to configuration # must be inserted to configuration
assert 'sell_profit_only' in config['experimental'] assert 'sell_profit_only' in default_conf['experimental']
assert not config['experimental']['sell_profit_only'] assert not default_conf['experimental']['sell_profit_only']
config = { default_conf.update({
'strategy': 'DefaultStrategy', 'strategy': 'DefaultStrategy',
'experimental': { 'experimental': {
'sell_profit_only': True, 'sell_profit_only': True,
}, },
} })
resolver = StrategyResolver(config) resolver = StrategyResolver(default_conf)
assert resolver.strategy.sell_profit_only assert resolver.strategy.sell_profit_only
assert isinstance(resolver.strategy.sell_profit_only, bool) assert isinstance(resolver.strategy.sell_profit_only, bool)
assert ('freqtrade.resolvers.strategy_resolver', assert log_has("Override strategy 'sell_profit_only' with value in config file: True.", caplog)
logging.INFO,
"Override strategy 'sell_profit_only' with value in config file: True."
) in caplog.record_tuples
def test_deprecate_populate_indicators(result): @pytest.mark.filterwarnings("ignore:deprecated")
def test_deprecate_populate_indicators(result, default_conf):
default_location = path.join(path.dirname(path.realpath(__file__))) default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', default_conf.update({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location}) 'strategy_path': default_location})
resolver = StrategyResolver(default_conf)
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered. # Cause all warnings to always be triggered.
warnings.simplefilter("always") warnings.simplefilter("always")
@@ -391,10 +368,12 @@ def test_deprecate_populate_indicators(result):
in str(w[-1].message) in str(w[-1].message)
def test_call_deprecated_function(result, monkeypatch): @pytest.mark.filterwarnings("ignore:deprecated")
def test_call_deprecated_function(result, monkeypatch, default_conf):
default_location = path.join(path.dirname(path.realpath(__file__))) default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', default_conf.update({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location}) 'strategy_path': default_location})
resolver = StrategyResolver(default_conf)
metadata = {'pair': 'ETH/BTC'} metadata = {'pair': 'ETH/BTC'}
# Make sure we are using a legacy function # Make sure we are using a legacy function

View File

@@ -3,7 +3,9 @@ import argparse
import pytest import pytest
from freqtrade.arguments import Arguments, TimeRange from freqtrade.configuration import Arguments
from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME
from freqtrade.configuration.cli_options import check_int_positive
# Parse common command-line-arguments. Used for all tools # Parse common command-line-arguments. Used for all tools
@@ -18,7 +20,7 @@ def test_parse_args_defaults() -> None:
assert args.config == ['config.json'] assert args.config == ['config.json']
assert args.strategy_path is None assert args.strategy_path is None
assert args.datadir is None assert args.datadir is None
assert args.loglevel == 0 assert args.verbosity == 0
def test_parse_args_config() -> None: def test_parse_args_config() -> None:
@@ -41,17 +43,17 @@ def test_parse_args_db_url() -> None:
def test_parse_args_verbose() -> None: def test_parse_args_verbose() -> None:
args = Arguments(['-v'], '').get_parsed_arg() args = Arguments(['-v'], '').get_parsed_arg()
assert args.loglevel == 1 assert args.verbosity == 1
args = Arguments(['--verbose'], '').get_parsed_arg() args = Arguments(['--verbose'], '').get_parsed_arg()
assert args.loglevel == 1 assert args.verbosity == 1
def test_common_scripts_options() -> None: def test_common_scripts_options() -> None:
arguments = Arguments(['-p', 'ETH/BTC'], '') args = Arguments(['download-data', '-p', 'ETH/BTC', 'XRP/BTC'], '').get_parsed_arg()
arguments.common_scripts_options()
args = arguments.get_parsed_arg() assert args.pairs == ['ETH/BTC', 'XRP/BTC']
assert args.pairs == 'ETH/BTC' assert hasattr(args, "func")
def test_parse_args_version() -> None: def test_parse_args_version() -> None:
@@ -84,45 +86,6 @@ def test_parse_args_strategy_path_invalid() -> None:
Arguments(['--strategy-path'], '').get_parsed_arg() Arguments(['--strategy-path'], '').get_parsed_arg()
def test_parse_args_dynamic_whitelist() -> None:
args = Arguments(['--dynamic-whitelist'], '').get_parsed_arg()
assert args.dynamic_whitelist == 20
def test_parse_args_dynamic_whitelist_10() -> None:
args = Arguments(['--dynamic-whitelist', '10'], '').get_parsed_arg()
assert args.dynamic_whitelist == 10
def test_parse_args_dynamic_whitelist_invalid_values() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['--dynamic-whitelist', 'abc'], '').get_parsed_arg()
def test_parse_timerange_incorrect() -> None:
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500')
assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-')
assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522')
timerange = Arguments.parse_timerange('20100522-20150730')
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
# Added test for unix timestamp - BTC genesis date
assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-')
assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000')
timerange = Arguments.parse_timerange('1231006505-1233360000')
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
# TODO: Find solution for the following case (passing timestamp in ms)
timerange = Arguments.parse_timerange('1231006505000-1233360000000')
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
Arguments.parse_timerange('-')
def test_parse_args_backtesting_invalid() -> None: def test_parse_args_backtesting_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'): with pytest.raises(SystemExit, match=r'2'):
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg() Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
@@ -135,7 +98,6 @@ def test_parse_args_backtesting_custom() -> None:
args = [ args = [
'-c', 'test_conf.json', '-c', 'test_conf.json',
'backtesting', 'backtesting',
'--live',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--refresh-pairs-cached', '--refresh-pairs-cached',
'--strategy-list', '--strategy-list',
@@ -144,8 +106,7 @@ def test_parse_args_backtesting_custom() -> None:
] ]
call_args = Arguments(args, '').get_parsed_arg() call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == ['test_conf.json'] assert call_args.config == ['test_conf.json']
assert call_args.live is True assert call_args.verbosity == 0
assert call_args.loglevel == 0
assert call_args.subparser == 'backtesting' assert call_args.subparser == 'backtesting'
assert call_args.func is not None assert call_args.func is not None
assert call_args.ticker_interval == '1m' assert call_args.ticker_interval == '1m'
@@ -164,7 +125,7 @@ def test_parse_args_hyperopt_custom() -> None:
call_args = Arguments(args, '').get_parsed_arg() call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == ['test_conf.json'] assert call_args.config == ['test_conf.json']
assert call_args.epochs == 20 assert call_args.epochs == 20
assert call_args.loglevel == 0 assert call_args.verbosity == 0
assert call_args.subparser == 'hyperopt' assert call_args.subparser == 'hyperopt'
assert call_args.spaces == ['buy'] assert call_args.spaces == ['buy']
assert call_args.func is not None assert call_args.func is not None
@@ -172,17 +133,16 @@ def test_parse_args_hyperopt_custom() -> None:
def test_download_data_options() -> None: def test_download_data_options() -> None:
args = [ args = [
'--datadir', 'datadir/directory',
'download-data',
'--pairs-file', 'file_with_pairs', '--pairs-file', 'file_with_pairs',
'--datadir', 'datadir/folder',
'--days', '30', '--days', '30',
'--exchange', 'binance' '--exchange', 'binance'
] ]
arguments = Arguments(args, '') args = Arguments(args, '').get_parsed_arg()
arguments.common_options()
arguments.download_data_options()
args = arguments.parse_args()
assert args.pairs_file == 'file_with_pairs' assert args.pairs_file == 'file_with_pairs'
assert args.datadir == 'datadir/folder' assert args.datadir == 'datadir/directory'
assert args.days == 30 assert args.days == 30
assert args.exchange == 'binance' assert args.exchange == 'binance'
@@ -195,29 +155,27 @@ def test_plot_dataframe_options() -> None:
'-p', 'UNITTEST/BTC', '-p', 'UNITTEST/BTC',
] ]
arguments = Arguments(args, '') arguments = Arguments(args, '')
arguments.common_scripts_options() arguments._build_args(ARGS_PLOT_DATAFRAME)
arguments.plot_dataframe_options() pargs = arguments._parse_args()
pargs = arguments.parse_args(True)
assert pargs.indicators1 == "sma10,sma100" assert pargs.indicators1 == "sma10,sma100"
assert pargs.indicators2 == "macd,fastd,fastk" assert pargs.indicators2 == "macd,fastd,fastk"
assert pargs.plot_limit == 30 assert pargs.plot_limit == 30
assert pargs.pairs == "UNITTEST/BTC" assert pargs.pairs == ["UNITTEST/BTC"]
def test_check_int_positive() -> None: def test_check_int_positive() -> None:
assert check_int_positive("3") == 3
assert Arguments.check_int_positive("3") == 3 assert check_int_positive("1") == 1
assert Arguments.check_int_positive("1") == 1 assert check_int_positive("100") == 100
assert Arguments.check_int_positive("100") == 100
with pytest.raises(argparse.ArgumentTypeError): with pytest.raises(argparse.ArgumentTypeError):
Arguments.check_int_positive("-2") check_int_positive("-2")
with pytest.raises(argparse.ArgumentTypeError): with pytest.raises(argparse.ArgumentTypeError):
Arguments.check_int_positive("0") check_int_positive("0")
with pytest.raises(argparse.ArgumentTypeError): with pytest.raises(argparse.ArgumentTypeError):
Arguments.check_int_positive("3.5") check_int_positive("3.5")
with pytest.raises(argparse.ArgumentTypeError): with pytest.raises(argparse.ArgumentTypeError):
Arguments.check_int_positive("DeadBeef") check_int_positive("DeadBeef")

View File

@@ -1,29 +1,33 @@
# pragma pylint: disable=missing-docstring, protected-access, invalid-name # pragma pylint: disable=missing-docstring, protected-access, invalid-name
import json import json
import logging import logging
from argparse import Namespace import warnings
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import Draft4Validator, ValidationError, validate from jsonschema import Draft4Validator, ValidationError, validate
from freqtrade import OperationalException, constants from freqtrade import OperationalException, constants
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments, Configuration, validate_config_consistency
from freqtrade.configuration import Configuration, set_loggers from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.config_validation import validate_config_schema
from freqtrade.configuration.directory_operations import (create_datadir,
create_userdata_dir)
from freqtrade.configuration.load_config import load_config_file
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
from freqtrade.loggers import _set_loggers
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.tests.conftest import log_has, log_has_re from freqtrade.tests.conftest import (log_has, log_has_re,
patched_configuration_load_config_file)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def all_conf(): def all_conf():
config_file = Path(__file__).parents[2] / "config_full.json.example" config_file = Path(__file__).parents[2] / "config_full.json.example"
print(config_file) print(config_file)
configuration = Configuration(Namespace()) conf = load_config_file(str(config_file))
conf = configuration._load_config_file(str(config_file))
return conf return conf
@@ -31,42 +35,63 @@ def test_load_config_invalid_pair(default_conf) -> None:
default_conf['exchange']['pair_whitelist'].append('ETH-BTC') default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
with pytest.raises(ValidationError, match=r'.*does not match.*'): with pytest.raises(ValidationError, match=r'.*does not match.*'):
configuration = Configuration(Namespace()) validate_config_schema(default_conf)
configuration._validate_config_schema(default_conf)
def test_load_config_missing_attributes(default_conf) -> None: def test_load_config_missing_attributes(default_conf) -> None:
default_conf.pop('exchange') default_conf.pop('exchange')
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'): with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
configuration = Configuration(Namespace()) validate_config_schema(default_conf)
configuration._validate_config_schema(default_conf)
def test_load_config_incorrect_stake_amount(default_conf) -> None: def test_load_config_incorrect_stake_amount(default_conf) -> None:
default_conf['stake_amount'] = 'fake' default_conf['stake_amount'] = 'fake'
with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'): with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'):
configuration = Configuration(Namespace()) validate_config_schema(default_conf)
configuration._validate_config_schema(default_conf)
def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None:
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( del default_conf['user_data_dir']
file_mock = mocker.patch('freqtrade.configuration.load_config.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
configuration = Configuration(Namespace()) validated_conf = load_config_file('somefile')
validated_conf = configuration._load_config_file('somefile')
assert file_mock.call_count == 1 assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items() assert validated_conf.items() >= default_conf.items()
def test__args_to_config(caplog):
arg_list = ['--strategy-path', 'TestTest']
args = Arguments(arg_list, '').get_parsed_arg()
configuration = Configuration(args)
config = {}
with warnings.catch_warnings(record=True) as w:
# No warnings ...
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef")
assert len(w) == 0
assert log_has("DeadBeef", caplog)
assert config['strategy_path'] == "TestTest"
configuration = Configuration(args)
config = {}
with warnings.catch_warnings(record=True) as w:
# Deprecation warnings!
configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef",
deprecated_msg="Going away soon!")
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "DEPRECATED: Going away soon!" in str(w[-1].message)
assert log_has("DeadBeef", caplog)
assert config['strategy_path'] == "TestTest"
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
default_conf['max_open_trades'] = 0 default_conf['max_open_trades'] = 0
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@@ -74,7 +99,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
assert validated_conf['max_open_trades'] == 0 assert validated_conf['max_open_trades'] == 0
assert 'internals' in validated_conf assert 'internals' in validated_conf
assert log_has('Validating configuration ...', caplog.record_tuples) assert log_has('Validating configuration ...', caplog)
def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
@@ -88,7 +113,10 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
config_files = [conf1, conf2] config_files = [conf1, conf2]
configsmock = MagicMock(side_effect=config_files) configsmock = MagicMock(side_effect=config_files)
mocker.patch('freqtrade.configuration.Configuration._load_config_file', configsmock) mocker.patch(
'freqtrade.configuration.configuration.load_config_file',
configsmock
)
arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ] arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ]
args = Arguments(arg_list, '').get_parsed_arg() args = Arguments(arg_list, '').get_parsed_arg()
@@ -103,14 +131,41 @@ def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist'] assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist']
assert 'internals' in validated_conf assert 'internals' in validated_conf
assert log_has('Validating configuration ...', caplog.record_tuples) assert log_has('Validating configuration ...', caplog)
def test_from_config(default_conf, mocker, caplog) -> None:
conf1 = deepcopy(default_conf)
conf2 = deepcopy(default_conf)
del conf1['exchange']['key']
del conf1['exchange']['secret']
del conf2['exchange']['name']
conf2['exchange']['pair_whitelist'] += ['NANO/BTC']
conf2['fiat_display_currency'] = "EUR"
config_files = [conf1, conf2]
configsmock = MagicMock(side_effect=config_files)
mocker.patch(
'freqtrade.configuration.configuration.load_config_file',
configsmock
)
validated_conf = Configuration.from_files(['test_conf.json', 'test2_conf.json'])
exchange_conf = default_conf['exchange']
assert validated_conf['exchange']['name'] == exchange_conf['name']
assert validated_conf['exchange']['key'] == exchange_conf['key']
assert validated_conf['exchange']['secret'] == exchange_conf['secret']
assert validated_conf['exchange']['pair_whitelist'] != conf1['exchange']['pair_whitelist']
assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist']
assert validated_conf['fiat_display_currency'] == "EUR"
assert 'internals' in validated_conf
assert log_has('Validating configuration ...', caplog)
def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None:
default_conf['max_open_trades'] = -1 default_conf['max_open_trades'] = -1
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@@ -118,26 +173,23 @@ def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) ->
assert validated_conf['max_open_trades'] > 999999999 assert validated_conf['max_open_trades'] > 999999999
assert validated_conf['max_open_trades'] == float('inf') assert validated_conf['max_open_trades'] == float('inf')
assert log_has('Validating configuration ...', caplog.record_tuples) assert log_has('Validating configuration ...', caplog)
assert "runmode" in validated_conf assert "runmode" in validated_conf
assert validated_conf['runmode'] == RunMode.DRY_RUN assert validated_conf['runmode'] == RunMode.DRY_RUN
def test_load_config_file_exception(mocker) -> None: def test_load_config_file_exception(mocker) -> None:
mocker.patch( mocker.patch(
'freqtrade.configuration.open', 'freqtrade.configuration.configuration.open',
MagicMock(side_effect=FileNotFoundError('File not found')) MagicMock(side_effect=FileNotFoundError('File not found'))
) )
configuration = Configuration(Namespace())
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'): with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
configuration._load_config_file('somefile') load_config_file('somefile')
def test_load_config(default_conf, mocker) -> None: def test_load_config(default_conf, mocker) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@@ -149,11 +201,9 @@ def test_load_config(default_conf, mocker) -> None:
def test_load_config_with_params(default_conf, mocker) -> None: def test_load_config_with_params(default_conf, mocker) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path', '--strategy-path', '/some/path',
'--db-url', 'sqlite:///someurl', '--db-url', 'sqlite:///someurl',
@@ -162,8 +212,6 @@ def test_load_config_with_params(default_conf, mocker) -> None:
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
assert validated_conf.get('pairlist', {}).get('method') == 'VolumePairList'
assert validated_conf.get('pairlist', {}).get('config').get('number_assets') == 10
assert validated_conf.get('strategy') == 'TestStrategy' assert validated_conf.get('strategy') == 'TestStrategy'
assert validated_conf.get('strategy_path') == '/some/path' assert validated_conf.get('strategy_path') == '/some/path'
assert validated_conf.get('db_url') == 'sqlite:///someurl' assert validated_conf.get('db_url') == 'sqlite:///someurl'
@@ -172,9 +220,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = False conf["dry_run"] = False
conf["db_url"] = "sqlite:///path/to/db.sqlite" conf["db_url"] = "sqlite:///path/to/db.sqlite"
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, conf)
read_data=json.dumps(conf)
))
arglist = [ arglist = [
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@@ -190,9 +236,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = True conf["dry_run"] = True
conf["db_url"] = "sqlite:///path/to/db.sqlite" conf["db_url"] = "sqlite:///path/to/db.sqlite"
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, conf)
read_data=json.dumps(conf)
))
arglist = [ arglist = [
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@@ -208,9 +252,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = False conf["dry_run"] = False
del conf["db_url"] del conf["db_url"]
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, conf)
read_data=json.dumps(conf)
))
arglist = [ arglist = [
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@@ -228,9 +270,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
conf = default_conf.copy() conf = default_conf.copy()
conf["dry_run"] = True conf["dry_run"] = True
conf["db_url"] = DEFAULT_DB_PROD_URL conf["db_url"] = DEFAULT_DB_PROD_URL
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, conf)
read_data=json.dumps(conf)
))
arglist = [ arglist = [
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@@ -248,9 +288,7 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
'strategy': 'CustomStrategy', 'strategy': 'CustomStrategy',
'strategy_path': '/tmp/strategies', 'strategy_path': '/tmp/strategies',
}) })
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@@ -261,11 +299,9 @@ def test_load_custom_strategy(default_conf, mocker) -> None:
def test_show_info(default_conf, mocker, caplog) -> None: def test_show_info(default_conf, mocker, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--db-url', 'sqlite:///tmp/testdb', '--db-url', 'sqlite:///tmp/testdb',
] ]
@@ -274,21 +310,13 @@ def test_show_info(default_conf, mocker, caplog) -> None:
configuration = Configuration(args) configuration = Configuration(args)
configuration.get_config() configuration.get_config()
assert log_has( assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog)
'Parameter --dynamic-whitelist has been deprecated, ' assert log_has('Dry run is enabled', caplog)
'and will be completely replaced by the whitelist dict in the future. '
'For now: using dynamically generated whitelist based on VolumePairList. '
'(not applicable with Backtesting and Hyperopt)',
caplog.record_tuples
)
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
assert log_has('Dry run is enabled', caplog.record_tuples)
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
@@ -305,39 +333,39 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'exchange' in config assert 'exchange' in config
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert 'user_data_dir' in config
'Using data folder: {} ...'.format(config['datadir']), assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
caplog.record_tuples
)
assert 'ticker_interval' in config assert 'ticker_interval' in config
assert not log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) assert not log_has('Parameter -i/--ticker-interval detected ...', caplog)
assert 'live' not in config
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'position_stacking' not in config assert 'position_stacking' not in config
assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) assert not log_has('Parameter --enable-position-stacking detected ...', caplog)
assert 'refresh_pairs' not in config assert 'refresh_pairs' not in config
assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
assert 'timerange' not in config assert 'timerange' not in config
assert 'export' not in config assert 'export' not in config
@pytest.mark.filterwarnings("ignore:DEPRECATED")
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf) mocker.patch(
)) 'freqtrade.configuration.configuration.create_datadir',
mocker.patch('freqtrade.configuration.Configuration._create_datadir', lambda s, c, x: x) lambda c, x: x
)
mocker.patch(
'freqtrade.configuration.configuration.create_userdata_dir',
lambda x, *args, **kwargs: Path(x)
)
arglist = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
'--userdir', "/tmp/freqtrade",
'backtesting', 'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--live',
'--enable-position-stacking', '--enable-position-stacking',
'--disable-max-market-positions', '--disable-max-market-positions',
'--refresh-pairs-cached', '--refresh-pairs-cached',
@@ -355,46 +383,35 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'exchange' in config assert 'exchange' in config
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has('Using data directory: {} ...'.format("/foo/bar"), caplog)
'Using data folder: {} ...'.format(config['datadir']), assert log_has('Using user-data directory: {} ...'.format("/tmp/freqtrade"), caplog)
caplog.record_tuples assert 'user_data_dir' in config
)
assert 'ticker_interval' in config assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
caplog.record_tuples) caplog)
assert 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'position_stacking'in config assert 'position_stacking'in config
assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) assert log_has('Parameter --enable-position-stacking detected ...', caplog)
assert 'use_max_market_positions' in config assert 'use_max_market_positions' in config
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples) assert log_has('Parameter --disable-max-market-positions detected ...', caplog)
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples) assert log_has('max_open_trades set to unlimited ...', caplog)
assert 'refresh_pairs'in config assert 'refresh_pairs'in config
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog)
assert 'timerange' in config assert 'timerange' in config
assert log_has( assert log_has('Parameter --timerange detected: {} ...'.format(config['timerange']), caplog)
'Parameter --timerange detected: {} ...'.format(config['timerange']),
caplog.record_tuples
)
assert 'export' in config assert 'export' in config
assert log_has( assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None: def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
""" """
Test setup_configuration() function Test setup_configuration() function
""" """
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
@@ -417,16 +434,13 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
assert 'exchange' in config assert 'exchange' in config
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog)
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...',
caplog.record_tuples) caplog)
assert 'strategy_list' in config assert 'strategy_list' in config
assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples) assert log_has('Using strategy list of 2 Strategies', caplog)
assert 'position_stacking' not in config assert 'position_stacking' not in config
@@ -435,16 +449,12 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non
assert 'timerange' not in config assert 'timerange' not in config
assert 'export' in config assert 'export' in config
assert log_has( assert log_has('Parameter --export detected: {} ...'.format(config['export']), caplog)
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'hyperopt', 'hyperopt',
'--epochs', '10', '--epochs', '10',
@@ -458,73 +468,67 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
assert 'epochs' in config assert 'epochs' in config
assert int(config['epochs']) == 10 assert int(config['epochs']) == 10
assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...', assert log_has('Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...',
caplog.record_tuples) caplog)
assert 'spaces' in config assert 'spaces' in config
assert config['spaces'] == ['all'] assert config['spaces'] == ['all']
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples) assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog)
assert "runmode" in config assert "runmode" in config
assert config['runmode'] == RunMode.HYPEROPT assert config['runmode'] == RunMode.HYPEROPT
def test_check_exchange(default_conf, caplog) -> None: def test_check_exchange(default_conf, caplog) -> None:
configuration = Configuration(Namespace())
# Test an officially supported by Freqtrade team exchange # Test an officially supported by Freqtrade team exchange
default_conf.get('exchange').update({'name': 'BITTREX'}) default_conf.get('exchange').update({'name': 'BITTREX'})
assert configuration.check_exchange(default_conf) assert check_exchange(default_conf)
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
caplog.record_tuples) caplog)
caplog.clear() caplog.clear()
# Test an officially supported by Freqtrade team exchange # Test an officially supported by Freqtrade team exchange
default_conf.get('exchange').update({'name': 'binance'}) default_conf.get('exchange').update({'name': 'binance'})
assert configuration.check_exchange(default_conf) assert check_exchange(default_conf)
assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.", assert log_has_re(r"Exchange .* is officially supported by the Freqtrade development team\.",
caplog.record_tuples) caplog)
caplog.clear() caplog.clear()
# Test an available exchange, supported by ccxt # Test an available exchange, supported by ccxt
default_conf.get('exchange').update({'name': 'kraken'}) default_conf.get('exchange').update({'name': 'kraken'})
assert configuration.check_exchange(default_conf) assert check_exchange(default_conf)
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
r"by the Freqtrade development team\. .*", r"by the Freqtrade development team\. .*", caplog)
caplog.record_tuples)
caplog.clear() caplog.clear()
# Test a 'bad' exchange, which known to have serious problems # Test a 'bad' exchange, which known to have serious problems
default_conf.get('exchange').update({'name': 'bitmex'}) default_conf.get('exchange').update({'name': 'bitmex'})
assert not configuration.check_exchange(default_conf) with pytest.raises(OperationalException,
assert log_has_re(r"Exchange .* is known to not work with the bot yet\. " match=r"Exchange .* is known to not work with the bot yet.*"):
r"Use it only for development and testing purposes\.", check_exchange(default_conf)
caplog.record_tuples)
caplog.clear() caplog.clear()
# Test a 'bad' exchange with check_for_bad=False # Test a 'bad' exchange with check_for_bad=False
default_conf.get('exchange').update({'name': 'bitmex'}) default_conf.get('exchange').update({'name': 'bitmex'})
assert configuration.check_exchange(default_conf, False) assert check_exchange(default_conf, False)
assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported " assert log_has_re(r"Exchange .* is supported by ccxt and .* not officially supported "
r"by the Freqtrade development team\. .*", r"by the Freqtrade development team\. .*", caplog)
caplog.record_tuples)
caplog.clear() caplog.clear()
# Test an invalid exchange # Test an invalid exchange
default_conf.get('exchange').update({'name': 'unknown_exchange'}) default_conf.get('exchange').update({'name': 'unknown_exchange'})
configuration.config = default_conf
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
match=r'.*Exchange "unknown_exchange" is not supported by ccxt ' match=r'.*Exchange "unknown_exchange" is not supported by ccxt '
r'and therefore not available for the bot.*' r'and therefore not available for the bot.*'
): ):
configuration.check_exchange(default_conf) check_exchange(default_conf)
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)))
# Prevent setting loggers # Prevent setting loggers
mocker.patch('freqtrade.configuration.set_loggers', MagicMock) mocker.patch('freqtrade.loggers._set_loggers', MagicMock)
arglist = ['-vvv'] arglist = ['-vvv']
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
@@ -532,7 +536,7 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
assert validated_conf.get('verbosity') == 3 assert validated_conf.get('verbosity') == 3
assert log_has('Verbosity set to 3', caplog.record_tuples) assert log_has('Verbosity set to 3', caplog)
def test_set_loggers() -> None: def test_set_loggers() -> None:
@@ -546,7 +550,7 @@ def test_set_loggers() -> None:
previous_value2 = logging.getLogger('ccxt.base.exchange').level previous_value2 = logging.getLogger('ccxt.base.exchange').level
previous_value3 = logging.getLogger('telegram').level previous_value3 = logging.getLogger('telegram').level
set_loggers() _set_loggers()
value1 = logging.getLogger('requests').level value1 = logging.getLogger('requests').level
assert previous_value1 is not value1 assert previous_value1 is not value1
@@ -560,13 +564,13 @@ def test_set_loggers() -> None:
assert previous_value3 is not value3 assert previous_value3 is not value3
assert value3 is logging.INFO assert value3 is logging.INFO
set_loggers(log_level=2) _set_loggers(verbosity=2)
assert logging.getLogger('requests').level is logging.DEBUG assert logging.getLogger('requests').level is logging.DEBUG
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
assert logging.getLogger('telegram').level is logging.INFO assert logging.getLogger('telegram').level is logging.INFO
set_loggers(log_level=3) _set_loggers(verbosity=3)
assert logging.getLogger('requests').level is logging.DEBUG assert logging.getLogger('requests').level is logging.DEBUG
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
@@ -574,8 +578,7 @@ def test_set_loggers() -> None:
def test_set_logfile(default_conf, mocker): def test_set_logfile(default_conf, mocker):
mocker.patch('freqtrade.configuration.open', patched_configuration_load_config_file(mocker, default_conf)
mocker.mock_open(read_data=json.dumps(default_conf)))
arglist = [ arglist = [
'--logfile', 'test_file.log', '--logfile', 'test_file.log',
@@ -592,33 +595,65 @@ def test_set_logfile(default_conf, mocker):
def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None:
default_conf['forcebuy_enable'] = True default_conf['forcebuy_enable'] = True
mocker.patch('freqtrade.configuration.open', mocker.mock_open( patched_configuration_load_config_file(mocker, default_conf)
read_data=json.dumps(default_conf)
))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
assert validated_conf.get('forcebuy_enable') assert validated_conf.get('forcebuy_enable')
assert log_has('`forcebuy` RPC message enabled.', caplog.record_tuples) assert log_has('`forcebuy` RPC message enabled.', caplog)
def test_validate_default_conf(default_conf) -> None: def test_validate_default_conf(default_conf) -> None:
validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) validate(default_conf, constants.CONF_SCHEMA, Draft4Validator)
def test__create_datadir(mocker, default_conf, caplog) -> None: def test_create_datadir(mocker, default_conf, caplog) -> None:
mocker.patch('os.path.isdir', MagicMock(return_value=False)) mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
md = MagicMock() md = mocker.patch.object(Path, 'mkdir', MagicMock())
mocker.patch('os.makedirs', md)
cfg = Configuration(Namespace()) create_datadir(default_conf, '/foo/bar')
cfg._create_datadir(default_conf, '/foo/bar') assert md.call_args[1]['parents'] is True
assert md.call_args[0][0] == "/foo/bar" assert log_has('Created data directory: /foo/bar', caplog)
assert log_has('Created data directory: /foo/bar', caplog.record_tuples)
def test_create_userdata_dir(mocker, default_conf, caplog) -> None:
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
md = mocker.patch.object(Path, 'mkdir', MagicMock())
x = create_userdata_dir('/tmp/bar', create_dir=True)
assert md.call_count == 7
assert md.call_args[1]['parents'] is False
assert log_has('Created user-data directory: /tmp/bar', caplog)
assert isinstance(x, Path)
assert str(x) == "/tmp/bar"
def test_create_userdata_dir_exists(mocker, default_conf, caplog) -> None:
mocker.patch.object(Path, "is_dir", MagicMock(return_value=True))
md = mocker.patch.object(Path, 'mkdir', MagicMock())
create_userdata_dir('/tmp/bar')
assert md.call_count == 0
def test_create_userdata_dir_exists_exception(mocker, default_conf, caplog) -> None:
mocker.patch.object(Path, "is_dir", MagicMock(return_value=False))
md = mocker.patch.object(Path, 'mkdir', MagicMock())
with pytest.raises(OperationalException, match=r'Directory `/tmp/bar` does not exist.*'):
create_userdata_dir('/tmp/bar', create_dir=False)
assert md.call_count == 0
def test_validate_tsl(default_conf): def test_validate_tsl(default_conf):
default_conf['stoploss'] = 0.0
with pytest.raises(OperationalException, match='The config stoploss needs to be different '
'from 0 to avoid problems with sell orders.'):
validate_config_consistency(default_conf)
default_conf['stoploss'] = -0.10
default_conf['trailing_stop'] = True default_conf['trailing_stop'] = True
default_conf['trailing_stop_positive'] = 0 default_conf['trailing_stop_positive'] = 0
default_conf['trailing_stop_positive_offset'] = 0 default_conf['trailing_stop_positive_offset'] = 0
@@ -627,21 +662,54 @@ def test_validate_tsl(default_conf):
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r'The config trailing_only_offset_is_reached needs ' match=r'The config trailing_only_offset_is_reached needs '
'trailing_stop_positive_offset to be more than 0 in your config.'): 'trailing_stop_positive_offset to be more than 0 in your config.'):
configuration = Configuration(Namespace()) validate_config_consistency(default_conf)
configuration._validate_config_consistency(default_conf)
default_conf['trailing_stop_positive_offset'] = 0.01 default_conf['trailing_stop_positive_offset'] = 0.01
default_conf['trailing_stop_positive'] = 0.015 default_conf['trailing_stop_positive'] = 0.015
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r'The config trailing_stop_positive_offset needs ' match=r'The config trailing_stop_positive_offset needs '
'to be greater than trailing_stop_positive_offset in your config.'): 'to be greater than trailing_stop_positive in your config.'):
configuration = Configuration(Namespace()) validate_config_consistency(default_conf)
configuration._validate_config_consistency(default_conf)
default_conf['trailing_stop_positive'] = 0.01 default_conf['trailing_stop_positive'] = 0.01
default_conf['trailing_stop_positive_offset'] = 0.015 default_conf['trailing_stop_positive_offset'] = 0.015
Configuration(Namespace()) validate_config_consistency(default_conf)
configuration._validate_config_consistency(default_conf)
# 0 trailing stop positive - results in "Order would trigger immediately"
default_conf['trailing_stop_positive'] = 0
default_conf['trailing_stop_positive_offset'] = 0.02
default_conf['trailing_only_offset_is_reached'] = False
with pytest.raises(OperationalException,
match='The config trailing_stop_positive needs to be different from 0 '
'to avoid problems with sell orders'):
validate_config_consistency(default_conf)
def test_validate_edge(edge_conf):
edge_conf.update({"pairlist": {
"method": "VolumePairList",
}})
with pytest.raises(OperationalException,
match="Edge and VolumePairList are incompatible, "
"Edge will override whatever pairs VolumePairlist selects."):
validate_config_consistency(edge_conf)
edge_conf.update({"pairlist": {
"method": "StaticPairList",
}})
validate_config_consistency(edge_conf)
def test_load_config_test_comments() -> None:
"""
Load config with comments
"""
config_file = Path(__file__).parents[0] / "config_test_comments.json"
print(config_file)
conf = load_config_file(str(config_file))
assert conf
def test_load_config_default_exchange(all_conf) -> None: def test_load_config_default_exchange(all_conf) -> None:
@@ -655,8 +723,7 @@ def test_load_config_default_exchange(all_conf) -> None:
with pytest.raises(ValidationError, with pytest.raises(ValidationError,
match=r'\'exchange\' is a required property'): match=r'\'exchange\' is a required property'):
configuration = Configuration(Namespace()) validate_config_schema(all_conf)
configuration._validate_config_schema(all_conf)
def test_load_config_default_exchange_name(all_conf) -> None: def test_load_config_default_exchange_name(all_conf) -> None:
@@ -670,8 +737,7 @@ def test_load_config_default_exchange_name(all_conf) -> None:
with pytest.raises(ValidationError, with pytest.raises(ValidationError,
match=r'\'name\' is a required property'): match=r'\'name\' is a required property'):
configuration = Configuration(Namespace()) validate_config_schema(all_conf)
configuration._validate_config_schema(all_conf)
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False), @pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
@@ -694,7 +760,115 @@ def test_load_config_default_subkeys(all_conf, keys) -> None:
assert subkey not in all_conf[key] assert subkey not in all_conf[key]
configuration = Configuration(Namespace()) validate_config_schema(all_conf)
configuration._validate_config_schema(all_conf)
assert subkey in all_conf[key] assert subkey in all_conf[key]
assert all_conf[key][subkey] == keys[2] assert all_conf[key][subkey] == keys[2]
def test_pairlist_resolving():
arglist = [
'download-data',
'--pairs', 'ETH/BTC', 'XRP/BTC',
'--exchange', 'binance'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
assert config['exchange']['name'] == 'binance'
def test_pairlist_resolving_with_config(mocker, default_conf):
patched_configuration_load_config_file(mocker, default_conf)
arglist = [
'--config', 'config.json',
'download-data',
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert config['pairs'] == default_conf['exchange']['pair_whitelist']
assert config['exchange']['name'] == default_conf['exchange']['name']
# Override pairs
arglist = [
'--config', 'config.json',
'download-data',
'--pairs', 'ETH/BTC', 'XRP/BTC',
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
assert config['exchange']['name'] == default_conf['exchange']['name']
def test_pairlist_resolving_with_config_pl(mocker, default_conf):
patched_configuration_load_config_file(mocker, default_conf)
load_mock = mocker.patch("freqtrade.configuration.configuration.json_load",
MagicMock(return_value=['XRP/BTC', 'ETH/BTC']))
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock()))
arglist = [
'--config', 'config.json',
'download-data',
'--pairs-file', 'pairs.json',
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert load_mock.call_count == 1
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
assert config['exchange']['name'] == default_conf['exchange']['name']
def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf):
patched_configuration_load_config_file(mocker, default_conf)
mocker.patch("freqtrade.configuration.configuration.json_load",
MagicMock(return_value=['XRP/BTC', 'ETH/BTC']))
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
arglist = [
'--config', 'config.json',
'download-data',
'--pairs-file', 'pairs.json',
]
args = Arguments(arglist, '').get_parsed_arg()
with pytest.raises(OperationalException, match=r"No pairs file found with path.*"):
configuration = Configuration(args)
configuration.get_config()
def test_pairlist_resolving_fallback(mocker):
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock()))
mocker.patch("freqtrade.configuration.configuration.json_load",
MagicMock(return_value=['XRP/BTC', 'ETH/BTC']))
arglist = [
'download-data',
'--exchange', 'binance'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
assert config['exchange']['name'] == 'binance'
assert config['datadir'] == str(Path.cwd() / "user_data/data/binance")

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,17 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock, PropertyMock
import pytest import pytest
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.configuration import Arguments
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main from freqtrade.main import main
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import (log_has, patch_exchange,
patched_configuration_load_config_file)
from freqtrade.worker import Worker from freqtrade.worker import Worker
@@ -20,14 +21,14 @@ def test_parse_args_backtesting(mocker) -> None:
further argument parsing is done in test_arguments.py further argument parsing is done in test_arguments.py
""" """
backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock()) backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock())
backtesting_mock.__name__ = PropertyMock("start_backtesting")
# it's sys.exit(0) at the end of backtesting # it's sys.exit(0) at the end of backtesting
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(['backtesting']) main(['backtesting'])
assert backtesting_mock.call_count == 1 assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0] call_args = backtesting_mock.call_args[0][0]
assert call_args.config == ['config.json'] assert call_args.config == ['config.json']
assert call_args.live is False assert call_args.verbosity == 0
assert call_args.loglevel == 0
assert call_args.subparser == 'backtesting' assert call_args.subparser == 'backtesting'
assert call_args.func is not None assert call_args.func is not None
assert call_args.ticker_interval is None assert call_args.ticker_interval is None
@@ -35,13 +36,14 @@ def test_parse_args_backtesting(mocker) -> None:
def test_main_start_hyperopt(mocker) -> None: def test_main_start_hyperopt(mocker) -> None:
hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock()) hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock())
hyperopt_mock.__name__ = PropertyMock("start_hyperopt")
# it's sys.exit(0) at the end of hyperopt # it's sys.exit(0) at the end of hyperopt
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(['hyperopt']) main(['hyperopt'])
assert hyperopt_mock.call_count == 1 assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0] call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == ['config.json'] assert call_args.config == ['config.json']
assert call_args.loglevel == 0 assert call_args.verbosity == 0
assert call_args.subparser == 'hyperopt' assert call_args.subparser == 'hyperopt'
assert call_args.func is not None assert call_args.func is not None
@@ -50,10 +52,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception)) mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception))
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@@ -62,18 +61,15 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(args) main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples) assert log_has('Using config: config.json.example ...', caplog)
assert log_has('Fatal exception!', caplog.record_tuples) assert log_has('Fatal exception!', caplog)
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock())
mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt)) mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt))
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@@ -82,8 +78,8 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(args) main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples) assert log_has('Using config: config.json.example ...', caplog)
assert log_has('SIGINT received, aborting ...', caplog.record_tuples) assert log_has('SIGINT received, aborting ...', caplog)
def test_main_operational_exception(mocker, default_conf, caplog) -> None: def test_main_operational_exception(mocker, default_conf, caplog) -> None:
@@ -93,10 +89,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
'freqtrade.worker.Worker._worker', 'freqtrade.worker.Worker._worker',
MagicMock(side_effect=OperationalException('Oh snap!')) MagicMock(side_effect=OperationalException('Oh snap!'))
) )
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@@ -105,8 +98,8 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
# Test Main + the KeyboardInterrupt exception # Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(args) main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples) assert log_has('Using config: config.json.example ...', caplog)
assert log_has('Oh snap!', caplog.record_tuples) assert log_has('Oh snap!', caplog)
def test_main_reload_conf(mocker, default_conf, caplog) -> None: def test_main_reload_conf(mocker, default_conf, caplog) -> None:
@@ -118,10 +111,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
State.RUNNING, State.RUNNING,
OperationalException("Oh snap!")]) OperationalException("Oh snap!")])
mocker.patch('freqtrade.worker.Worker._worker', worker_mock) mocker.patch('freqtrade.worker.Worker._worker', worker_mock)
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock()) reconfigure_mock = mocker.patch('freqtrade.main.Worker._reconfigure', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@@ -132,7 +122,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
main(['-c', 'config.json.example']) main(['-c', 'config.json.example'])
assert log_has('Using config: config.json.example ...', caplog.record_tuples) assert log_has('Using config: config.json.example ...', caplog)
assert worker_mock.call_count == 4 assert worker_mock.call_count == 4
assert reconfigure_mock.call_count == 1 assert reconfigure_mock.call_count == 1
assert isinstance(worker.freqtrade, FreqtradeBot) assert isinstance(worker.freqtrade, FreqtradeBot)
@@ -145,10 +135,7 @@ def test_reconfigure(mocker, default_conf) -> None:
'freqtrade.worker.Worker._worker', 'freqtrade.worker.Worker._worker',
MagicMock(side_effect=OperationalException('Oh snap!')) MagicMock(side_effect=OperationalException('Oh snap!'))
) )
mocker.patch( patched_configuration_load_config_file(mocker, default_conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@@ -159,10 +146,7 @@ def test_reconfigure(mocker, default_conf) -> None:
# Renew mock to return modified data # Renew mock to return modified data
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['stake_amount'] += 1 conf['stake_amount'] += 1
mocker.patch( patched_configuration_load_config_file(mocker, conf)
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: conf
)
worker._config = conf worker._config = conf
# reconfigure should return a new instance # reconfigure should return a new instance

View File

@@ -1,13 +1,13 @@
# pragma pylint: disable=missing-docstring,C0103 # pragma pylint: disable=missing-docstring,C0103
import datetime import datetime
from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, from freqtrade.data.history import pair_data_filename
file_dump_json, file_load_json, format_ms_time, shorten_date) from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
from freqtrade.data.history import load_tickerdata_file, pair_data_filename file_load_json, format_ms_time, shorten_date)
from freqtrade.strategy.default_strategy import DefaultStrategy
def test_shorten_date() -> None: def test_shorten_date() -> None:
@@ -32,29 +32,15 @@ def test_datesarray_to_datetimearray(ticker_history_list):
assert date_len == 2 assert date_len == 2
def test_common_datearray(default_conf) -> None:
strategy = DefaultStrategy(default_conf)
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': parse_ticker_dataframe(tick, "1m", pair="UNITTEST/BTC",
fill_missing=True)}
dataframes = strategy.tickerdata_to_dataframe(tickerlist)
dates = common_datearray(dataframes)
assert dates.size == dataframes['UNITTEST/BTC']['date'].size
assert dates[0] == dataframes['UNITTEST/BTC']['date'][0]
assert dates[-1] == dataframes['UNITTEST/BTC']['date'].iloc[-1]
def test_file_dump_json(mocker) -> None: def test_file_dump_json(mocker) -> None:
file_open = mocker.patch('freqtrade.misc.open', MagicMock()) file_open = mocker.patch('freqtrade.misc.open', MagicMock())
json_dump = mocker.patch('rapidjson.dump', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3]) file_dump_json(Path('somefile'), [1, 2, 3])
assert file_open.call_count == 1 assert file_open.call_count == 1
assert json_dump.call_count == 1 assert json_dump.call_count == 1
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock()) file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
json_dump = mocker.patch('rapidjson.dump', MagicMock()) json_dump = mocker.patch('rapidjson.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3], True) file_dump_json(Path('somefile'), [1, 2, 3], True)
assert file_open.call_count == 1 assert file_open.call_count == 1
assert json_dump.call_count == 1 assert json_dump.call_count == 1

View File

@@ -151,7 +151,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog):
assert trade.close_date is None assert trade.close_date is None
assert log_has("LIMIT_BUY has been fulfilled for Trade(id=2, " assert log_has("LIMIT_BUY has been fulfilled for Trade(id=2, "
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).", "pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
caplog.record_tuples) caplog)
caplog.clear() caplog.clear()
trade.open_order_id = 'something' trade.open_order_id = 'something'
@@ -162,7 +162,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog):
assert trade.close_date is not None assert trade.close_date is not None
assert log_has("LIMIT_SELL has been fulfilled for Trade(id=2, " assert log_has("LIMIT_SELL has been fulfilled for Trade(id=2, "
"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).", "pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).",
caplog.record_tuples) caplog)
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@@ -184,7 +184,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
assert trade.close_date is None assert trade.close_date is None
assert log_has("MARKET_BUY has been fulfilled for Trade(id=1, " assert log_has("MARKET_BUY has been fulfilled for Trade(id=1, "
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).", "pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
caplog.record_tuples) caplog)
caplog.clear() caplog.clear()
trade.open_order_id = 'something' trade.open_order_id = 'something'
@@ -195,7 +195,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
assert trade.close_date is not None assert trade.close_date is not None
assert log_has("MARKET_SELL has been fulfilled for Trade(id=1, " assert log_has("MARKET_SELL has been fulfilled for Trade(id=1, "
"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).", "pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).",
caplog.record_tuples) caplog)
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@@ -558,10 +558,9 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
assert trade.ticker_interval is None assert trade.ticker_interval is None
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
assert trade.stoploss_last_update is None assert trade.stoploss_last_update is None
assert log_has("trying trades_bak1", caplog.record_tuples) assert log_has("trying trades_bak1", caplog)
assert log_has("trying trades_bak2", caplog.record_tuples) assert log_has("trying trades_bak2", caplog)
assert log_has("Running database migration - backup available as trades_bak2", assert log_has("Running database migration - backup available as trades_bak2", caplog)
caplog.record_tuples)
def test_migrate_mid_state(mocker, default_conf, fee, caplog): def test_migrate_mid_state(mocker, default_conf, fee, caplog):
@@ -621,9 +620,8 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
assert trade.max_rate == 0.0 assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0 assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0 assert trade.initial_stop_loss == 0.0
assert log_has("trying trades_bak0", caplog.record_tuples) assert log_has("trying trades_bak0", caplog)
assert log_has("Running database migration - backup available as trades_bak0", assert log_has("Running database migration - backup available as trades_bak0", caplog)
caplog.record_tuples)
def test_adjust_stop_loss(fee): def test_adjust_stop_loss(fee):

View File

@@ -1,31 +1,35 @@
from copy import deepcopy
from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
from plotly import tools import plotly.graph_objects as go
import plotly.graph_objs as go from plotly.subplots import make_subplots
from copy import deepcopy
from freqtrade.arguments import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import load_backtest_data from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
from freqtrade.plot.plotting import (generate_graph, generate_plot_file, from freqtrade.plot.plotting import (add_indicators, add_profit,
generate_row, plot_trades) generate_candlestick_graph,
generate_plot_filename,
generate_profit_graph, init_plotscript,
plot_trades, store_plot_file)
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.tests.conftest import log_has, log_has_re from freqtrade.tests.conftest import log_has, log_has_re
def fig_generating_mock(fig, *args, **kwargs): def fig_generating_mock(fig, *args, **kwargs):
""" Return Fig - used to mock generate_row and plot_trades""" """ Return Fig - used to mock add_indicators and plot_trades"""
return fig return fig
def find_trace_in_fig_data(data, search_string: str): def find_trace_in_fig_data(data, search_string: str):
matches = filter(lambda x: x.name == search_string, data) matches = (d for d in data if d.name == search_string)
return next(matches) return next(matches)
def generage_empty_figure(): def generage_empty_figure():
return tools.make_subplots( return make_subplots(
rows=3, rows=3,
cols=1, cols=1,
shared_xaxes=True, shared_xaxes=True,
@@ -34,7 +38,27 @@ def generage_empty_figure():
) )
def test_generate_row(default_conf, caplog): def test_init_plotscript(default_conf, mocker):
default_conf['timerange'] = "20180110-20180112"
default_conf['trade_source'] = "file"
default_conf['ticker_interval'] = "5m"
default_conf["datadir"] = history.make_testdata_path(None)
default_conf['exportfilename'] = str(
history.make_testdata_path(None) / "backtest-result_test.json")
ret = init_plotscript(default_conf)
assert "tickers" in ret
assert "trades" in ret
assert "pairs" in ret
assert "strategy" in ret
default_conf['pairs'] = ["POWR/BTC", "XLM/BTC"]
ret = init_plotscript(default_conf)
assert "tickers" in ret
assert "POWR/BTC" in ret["tickers"]
assert "XLM/BTC" in ret["tickers"]
def test_add_indicators(default_conf, caplog):
pair = "UNITTEST/BTC" pair = "UNITTEST/BTC"
timerange = TimeRange(None, 'line', 0, -1000) timerange = TimeRange(None, 'line', 0, -1000)
@@ -49,22 +73,22 @@ def test_generate_row(default_conf, caplog):
fig = generage_empty_figure() fig = generage_empty_figure()
# Row 1 # Row 1
fig1 = generate_row(fig=deepcopy(fig), row=1, indicators=indicators1, data=data) fig1 = add_indicators(fig=deepcopy(fig), row=1, indicators=indicators1, data=data)
figure = fig1.layout.figure figure = fig1.layout.figure
ema10 = find_trace_in_fig_data(figure.data, "ema10") ema10 = find_trace_in_fig_data(figure.data, "ema10")
assert isinstance(ema10, go.Scatter) assert isinstance(ema10, go.Scatter)
assert ema10.yaxis == "y" assert ema10.yaxis == "y"
fig2 = generate_row(fig=deepcopy(fig), row=3, indicators=indicators2, data=data) fig2 = add_indicators(fig=deepcopy(fig), row=3, indicators=indicators2, data=data)
figure = fig2.layout.figure figure = fig2.layout.figure
macd = find_trace_in_fig_data(figure.data, "macd") macd = find_trace_in_fig_data(figure.data, "macd")
assert isinstance(macd, go.Scatter) assert isinstance(macd, go.Scatter)
assert macd.yaxis == "y3" assert macd.yaxis == "y3"
# No indicator found # No indicator found
fig3 = generate_row(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data) fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data)
assert fig == fig3 assert fig == fig3
assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog.record_tuples) assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog)
def test_plot_trades(caplog): def test_plot_trades(caplog):
@@ -72,7 +96,7 @@ def test_plot_trades(caplog):
# nothing happens when no trades are available # nothing happens when no trades are available
fig = plot_trades(fig1, None) fig = plot_trades(fig1, None)
assert fig == fig1 assert fig == fig1
assert log_has("No trades found.", caplog.record_tuples) assert log_has("No trades found.", caplog)
pair = "ADA/BTC" pair = "ADA/BTC"
filename = history.make_testdata_path(None) / "backtest-result_test.json" filename = history.make_testdata_path(None) / "backtest-result_test.json"
trades = load_backtest_data(filename) trades = load_backtest_data(filename)
@@ -95,8 +119,8 @@ def test_plot_trades(caplog):
assert trade_sell.marker.color == 'red' assert trade_sell.marker.color == 'red'
def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog): def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, caplog):
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
MagicMock(side_effect=fig_generating_mock)) MagicMock(side_effect=fig_generating_mock))
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
MagicMock(side_effect=fig_generating_mock)) MagicMock(side_effect=fig_generating_mock))
@@ -110,8 +134,8 @@ def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog):
indicators1 = [] indicators1 = []
indicators2 = [] indicators2 = []
fig = generate_graph(pair=pair, data=data, trades=None, fig = generate_candlestick_graph(pair=pair, data=data, trades=None,
indicators1=indicators1, indicators2=indicators2) indicators1=indicators1, indicators2=indicators2)
assert isinstance(fig, go.Figure) assert isinstance(fig, go.Figure)
assert fig.layout.title.text == pair assert fig.layout.title.text == pair
figure = fig.layout.figure figure = fig.layout.figure
@@ -127,12 +151,12 @@ def test_generate_graph_no_signals_no_trades(default_conf, mocker, caplog):
assert row_mock.call_count == 2 assert row_mock.call_count == 2
assert trades_mock.call_count == 1 assert trades_mock.call_count == 1
assert log_has("No buy-signals found.", caplog.record_tuples) assert log_has("No buy-signals found.", caplog)
assert log_has("No sell-signals found.", caplog.record_tuples) assert log_has("No sell-signals found.", caplog)
def test_generate_graph_no_trades(default_conf, mocker): def test_generate_candlestick_graph_no_trades(default_conf, mocker):
row_mock = mocker.patch('freqtrade.plot.plotting.generate_row', row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators',
MagicMock(side_effect=fig_generating_mock)) MagicMock(side_effect=fig_generating_mock))
trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades', trades_mock = mocker.patch('freqtrade.plot.plotting.plot_trades',
MagicMock(side_effect=fig_generating_mock)) MagicMock(side_effect=fig_generating_mock))
@@ -147,8 +171,8 @@ def test_generate_graph_no_trades(default_conf, mocker):
indicators1 = [] indicators1 = []
indicators2 = [] indicators2 = []
fig = generate_graph(pair=pair, data=data, trades=None, fig = generate_candlestick_graph(pair=pair, data=data, trades=None,
indicators1=indicators1, indicators2=indicators2) indicators1=indicators1, indicators2=indicators2)
assert isinstance(fig, go.Figure) assert isinstance(fig, go.Figure)
assert fig.layout.title.text == pair assert fig.layout.title.text == pair
figure = fig.layout.figure figure = fig.layout.figure
@@ -178,12 +202,71 @@ def test_generate_graph_no_trades(default_conf, mocker):
assert trades_mock.call_count == 1 assert trades_mock.call_count == 1
def test_generate_Plot_filename():
fn = generate_plot_filename("UNITTEST/BTC", "5m")
assert fn == "freqtrade-plot-UNITTEST_BTC-5m.html"
def test_generate_plot_file(mocker, caplog): def test_generate_plot_file(mocker, caplog):
fig = generage_empty_figure() fig = generage_empty_figure()
plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock()) plot_mock = mocker.patch("freqtrade.plot.plotting.plot", MagicMock())
generate_plot_file(fig, "UNITTEST/BTC", "5m") store_plot_file(fig, filename="freqtrade-plot-UNITTEST_BTC-5m.html",
directory=Path("user_data/plots"))
assert plot_mock.call_count == 1 assert plot_mock.call_count == 1
assert plot_mock.call_args[0][0] == fig assert plot_mock.call_args[0][0] == fig
assert (plot_mock.call_args_list[0][1]['filename'] assert (plot_mock.call_args_list[0][1]['filename']
== "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html") == "user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html")
assert log_has("Stored plot as user_data/plots/freqtrade-plot-UNITTEST_BTC-5m.html",
caplog)
def test_add_profit():
filename = history.make_testdata_path(None) / "backtest-result_test.json"
bt_data = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112")
df = history.load_pair_history(pair="POWR/BTC", ticker_interval='5m',
datadir=None, timerange=timerange)
fig = generage_empty_figure()
cum_profits = create_cum_profit(df.set_index('date'),
bt_data[bt_data["pair"] == 'POWR/BTC'],
"cum_profits")
fig1 = add_profit(fig, row=2, data=cum_profits, column='cum_profits', name='Profits')
figure = fig1.layout.figure
profits = find_trace_in_fig_data(figure.data, "Profits")
assert isinstance(profits, go.Scattergl)
assert profits.yaxis == "y2"
def test_generate_profit_graph():
filename = history.make_testdata_path(None) / "backtest-result_test.json"
trades = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112")
pairs = ["POWR/BTC", "XLM/BTC"]
tickers = history.load_data(datadir=None,
pairs=pairs,
ticker_interval='5m',
timerange=timerange
)
trades = trades[trades['pair'].isin(pairs)]
fig = generate_profit_graph(pairs, tickers, trades)
assert isinstance(fig, go.Figure)
assert fig.layout.title.text == "Profit plot"
figure = fig.layout.figure
assert len(figure.data) == 4
avgclose = find_trace_in_fig_data(figure.data, "Avg close price")
assert isinstance(avgclose, go.Scattergl)
profit = find_trace_in_fig_data(figure.data, "Profit")
assert isinstance(profit, go.Scattergl)
for pair in pairs:
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")
assert isinstance(profit_pair, go.Scattergl)

View File

@@ -13,4 +13,4 @@ def test_talib_bollingerbands_near_zero_values():
{'close': 0.00000014} {'close': 0.00000014}
]) ])
bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2) bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2)
assert (bollinger['upperband'][3] != bollinger['middleband'][3]) assert bollinger['upperband'][3] != bollinger['middleband'][3]

View File

@@ -0,0 +1,28 @@
# pragma pylint: disable=missing-docstring, C0103
import pytest
from freqtrade.configuration import TimeRange
def test_parse_timerange_incorrect() -> None:
assert TimeRange(None, 'line', 0, -200) == TimeRange.parse_timerange('-200')
assert TimeRange('line', None, 200, 0) == TimeRange.parse_timerange('200-')
assert TimeRange('index', 'index', 200, 500) == TimeRange.parse_timerange('200-500')
assert TimeRange('date', None, 1274486400, 0) == TimeRange.parse_timerange('20100522-')
assert TimeRange(None, 'date', 0, 1274486400) == TimeRange.parse_timerange('-20100522')
timerange = TimeRange.parse_timerange('20100522-20150730')
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
# Added test for unix timestamp - BTC genesis date
assert TimeRange('date', None, 1231006505, 0) == TimeRange.parse_timerange('1231006505-')
assert TimeRange(None, 'date', 0, 1233360000) == TimeRange.parse_timerange('-1233360000')
timerange = TimeRange.parse_timerange('1231006505-1233360000')
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
# TODO: Find solution for the following case (passing timestamp in ms)
timerange = TimeRange.parse_timerange('1231006505000-1233360000000')
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
TimeRange.parse_timerange('-')

View File

@@ -1,8 +1,13 @@
from freqtrade.utils import setup_utils_configuration, start_list_exchanges
from freqtrade.tests.conftest import get_args
from freqtrade.state import RunMode
import re import re
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade.state import RunMode
from freqtrade.tests.conftest import get_args, log_has, patch_exchange
from freqtrade.utils import (setup_utils_configuration, start_create_userdir,
start_download_data, start_list_exchanges)
def test_setup_utils_configuration(): def test_setup_utils_configuration():
@@ -40,3 +45,110 @@ def test_list_exchanges(capsys):
assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out) assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out)
assert re.search(r"^binance$", captured.out, re.MULTILINE) assert re.search(r"^binance$", captured.out, re.MULTILINE)
assert re.search(r"^bittrex$", captured.out, re.MULTILINE) assert re.search(r"^bittrex$", captured.out, re.MULTILINE)
def test_create_datadir_failed(caplog):
args = [
"create-userdir",
]
with pytest.raises(SystemExit):
start_create_userdir(get_args(args))
assert log_has("`create-userdir` requires --userdir to be set.", caplog)
def test_create_datadir(caplog, mocker):
cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock())
args = [
"create-userdir",
"--userdir",
"/temp/freqtrade/test"
]
start_create_userdir(get_args(args))
assert cud.call_count == 1
assert len(caplog.record_tuples) == 0
def test_download_data(mocker, markets, caplog):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch.object(Path, "unlink", MagicMock())
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
"--erase",
]
start_download_data(get_args(args))
assert dl_mock.call_count == 4
assert dl_mock.call_args[1]['timerange'].starttype is None
assert dl_mock.call_args[1]['timerange'].stoptype is None
assert log_has("Deleting existing data for pair ETH/BTC, interval 1m.", caplog)
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
def test_download_data_days(mocker, markets, caplog):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch.object(Path, "unlink", MagicMock())
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
"--days", "20",
]
start_download_data(get_args(args))
assert dl_mock.call_count == 4
assert dl_mock.call_args[1]['timerange'].starttype == 'date'
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
def test_download_data_no_markets(mocker, caplog):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
)
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
]
start_download_data(get_args(args))
assert dl_mock.call_count == 0
assert log_has("Skipping pair ETH/BTC...", caplog)
assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog)
def test_download_data_keyboardInterrupt(mocker, caplog, markets):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history',
MagicMock(side_effect=KeyboardInterrupt))
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
]
with pytest.raises(SystemExit):
start_download_data(get_args(args))
assert dl_mock.call_count == 1

Some files were not shown because too many files have changed in this diff Show More