Compare commits

..

782 Commits

Author SHA1 Message Date
Samuel Husso
e63e808521 Merge pull request #1252 from freqtrade/release-0.17.2
Release 0.17.2
2018-10-03 13:09:11 +03:00
Samuel Husso
d549fe351c Prepare master for release 0.17.2 2018-10-02 09:24:22 +03:00
Matthias
9137338771 Merge pull request #1251 from freqtrade/pyup-scheduled-update-2018-10-01
Scheduled daily dependency update on monday
2018-10-01 19:27:17 +02:00
pyup-bot
d0c7b7c582 Update ccxt from 1.17.360 to 1.17.363 2018-10-01 14:29:06 +02:00
Matthias
b130a923f7 Merge pull request #1249 from freqtrade/pyup-scheduled-update-2018-09-30
Scheduled daily dependency update on sunday
2018-09-30 17:03:15 +02:00
Matthias
3af3094a56 Merge pull request #1247 from freqtrade/fix_hyperopt_pickle
Fix hyperopt pickle
2018-09-30 16:51:33 +02:00
pyup-bot
9d70d25064 Update scikit-learn from 0.19.2 to 0.20.0 2018-09-30 14:28:07 +02:00
pyup-bot
05adebb536 Update ccxt from 1.17.351 to 1.17.360 2018-09-30 14:28:06 +02:00
Matthias
e1ddddad4f Merge pull request #1246 from freqtrade/fix/network_test
Patch exchange to not cause network delays during tests
2018-09-30 08:42:38 +02:00
Matthias
84622dc84b Move test for strategy out of constructor 2018-09-29 14:23:53 +02:00
Matthias
36e9abc841 Manually update scikit-learn to 0.20.0 2018-09-29 13:50:02 +02:00
Matthias
1b290ffb5d Update hyperopt to show errors if non-supported variables are used 2018-09-29 13:49:38 +02:00
Matthias
334e7553e1 Fix hyperopt not working after update of scikit-learn to 0.20.0 2018-09-29 13:49:27 +02:00
Matthias
f4585a2745 Patch exchange to not cause network delays during tests 2018-09-29 13:35:48 +02:00
Matthias
448f3a7197 Merge pull request #1241 from freqtrade/fix/loadstrategyonce
Only load strategy once during backtesting
2018-09-29 09:12:41 +02:00
Matthias
6e66763e5f Only load strategy once during backtesting 2018-09-27 19:23:55 +02:00
Matthias
89b515be60 Merge pull request #1220 from freqtrade/fix/plot_dataframe
Fix plot dataframe
2018-09-27 12:40:34 +02:00
Matthias
d481895763 Merge pull request #1211 from freqtrade/fix_no_trades_found
Add offset to "get_trades_for_order"
2018-09-27 12:40:17 +02:00
Matthias
4ad3e96a2f Merge pull request #1225 from freqtrade/test_acl_improvement
Remove direct call to pytest fixture to elliminate pytest warning
2018-09-27 12:39:56 +02:00
Matthias
3893b638fe Merge pull request #1213 from freqtrade/fix_mac_install
Fix mac install documentation
2018-09-27 12:39:42 +02:00
Matthias
5dac3b5664 Merge pull request #1238 from freqtrade/fix/buyexception
Fix exception when order cannot be found
2018-09-26 19:26:17 +02:00
Matthias
bcb13d041e Merge pull request #1239 from freqtrade/pyup-scheduled-update-2018-09-26
Scheduled daily dependency update on wednesday
2018-09-26 19:25:50 +02:00
pyup-bot
f790f95319 Update ccxt from 1.17.350 to 1.17.351 2018-09-26 14:28:07 +02:00
Matthias
766d32897d Merge pull request #1204 from freqtrade/move_load_markets
refactor load_markets out of validate_pairs
2018-09-26 06:38:37 +02:00
Matthias
e09674b77f Merge pull request #1227 from freqtrade/feat/reduce_backtestnoise
don't print "NAN" lines in "left_open_trades"
2018-09-26 06:37:33 +02:00
Matthias
88ccdc0366 Fix exception when order cannot be found 2018-09-25 20:45:01 +02:00
Matthias
d04247cd9e Merge pull request #1235 from freqtrade/pyup-scheduled-update-2018-09-25
Scheduled daily dependency update on tuesday
2018-09-25 19:20:54 +02:00
pyup-bot
d13e87d7a4 Update ccxt from 1.17.341 to 1.17.350 2018-09-25 14:28:07 +02:00
Matthias
bbcbf6adc8 Merge pull request #1234 from freqtrade/pyup-scheduled-update-2018-09-23
Scheduled daily dependency update on sunday
2018-09-23 19:20:57 +02:00
pyup-bot
6116c27aa9 Update pytest from 3.8.0 to 3.8.1 2018-09-23 14:28:09 +02:00
pyup-bot
12e6287875 Update numpy from 1.15.1 to 1.15.2 2018-09-23 14:28:08 +02:00
pyup-bot
0e168159c1 Update ccxt from 1.17.335 to 1.17.341 2018-09-23 14:28:06 +02:00
Matthias
e1c9b77c44 Merge pull request #1230 from freqtrade/pyup-scheduled-update-2018-09-22
Scheduled daily dependency update on saturday
2018-09-22 15:44:51 +02:00
pyup-bot
54b714ba3f Update ccxt from 1.17.327 to 1.17.335 2018-09-22 14:28:05 +02:00
Matthias
f302882f67 Merge pull request #1228 from freqtrade/pyup-scheduled-update-2018-09-21
Scheduled daily dependency update on friday
2018-09-21 16:03:29 +02:00
pyup-bot
8e659af580 Update ccxt from 1.17.324 to 1.17.327 2018-09-21 14:28:07 +02:00
Matthias
567211e9f9 don't print "NAN" lines in "left_open_trades" 2018-09-20 20:35:26 +02:00
Matthias
95f884f4f3 Merge pull request #1226 from freqtrade/pyup-scheduled-update-2018-09-20
Scheduled daily dependency update on thursday
2018-09-20 19:22:08 +02:00
pyup-bot
53c0f01bef Update sqlalchemy from 1.2.11 to 1.2.12 2018-09-20 14:28:10 +02:00
pyup-bot
0aa8557c03 Update ccxt from 1.17.316 to 1.17.324 2018-09-20 14:28:08 +02:00
Matthias
4d5e368c2e Remove direct call to pytest fixture to elliminate pytest warning 2018-09-19 19:40:32 +02:00
Matthias
2d4d1d7306 Merge pull request #1224 from freqtrade/pyup-scheduled-update-2018-09-19
Scheduled daily dependency update on wednesday
2018-09-19 19:14:47 +02:00
pyup-bot
2c5b6aca91 Update ccxt from 1.17.311 to 1.17.316 2018-09-19 14:28:06 +02:00
Matthias
eaa657aa3b Merge pull request #1222 from freqtrade/pyup-scheduled-update-2018-09-18
Scheduled daily dependency update on tuesday
2018-09-18 19:15:01 +02:00
pyup-bot
a5d4de8037 Update ccxt from 1.17.305 to 1.17.311 2018-09-18 14:28:06 +02:00
Matthias
52b75c5997 Merge pull request #1218 from jin10086/develop
use --no-cache-dir for docker build
2018-09-17 20:49:55 +02:00
Matthias
f04e4f2123 Fix trailing whitespace 2018-09-17 20:49:41 +02:00
Matthias
176bae2d59 Set default-db url in configuration, not arguments
* Fixes a bug in plot_dataframe.py (#1217)
* db_url is eventually overwritten here anyway.
2018-09-17 19:57:47 +02:00
Matthias
14e21765f2 Fix missing column to load current backtesting export files 2018-09-17 19:44:40 +02:00
Matthias
eebaede80d Merge pull request #1219 from freqtrade/pyup-scheduled-update-2018-09-17
Scheduled daily dependency update on monday
2018-09-17 19:20:00 +02:00
pyup-bot
9b83a09224 Update ccxt from 1.17.300 to 1.17.305 2018-09-17 14:28:06 +02:00
gaojin
0a4b2f19e3 use --no-cache-dir for docker build
use --no-cache can save about 90M
```
➜  freqtrade git:(develop) ✗ docker images freq
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
freq                latest              b15db8341067        7 minutes ago       800MB
➜  freqtrade git:(develop) ✗ docker images freq_nocache
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
freq_nocache        latest              e5731f28ac54        20 seconds ago      709MB
```
2018-09-17 10:37:25 +08:00
Matthias
3abc294e5f Merge pull request #1216 from 0xflotus/patch-1
fixed being
2018-09-16 20:11:08 +02:00
0xflotus
6aa18bddc9 fixed being 2018-09-16 17:34:01 +02:00
Matthias
16279bc171 Merge pull request #1215 from freqtrade/pyup-scheduled-update-2018-09-16
Scheduled daily dependency update on sunday
2018-09-16 15:12:43 +02:00
pyup-bot
14961e2e38 Update ccxt from 1.17.294 to 1.17.300 2018-09-16 14:28:06 +02:00
Matthias
30ae5829f5 Fix SED command for macos
Mac uses the bsd version, where -i without backup is not allowed.
2018-09-16 11:26:20 +02:00
Matthias
200dfa7575 Wording for readme.md 2018-09-16 11:22:15 +02:00
Matthias
51b3eb78d7 Add section about about clock accuracy to readme.md 2018-09-15 20:38:09 +02:00
Matthias
9685c09c1a Add offset to "get_trades_for_order" 2018-09-15 20:28:36 +02:00
Matthias
4303e86e09 Merge pull request #1210 from freqtrade/pyup-scheduled-update-2018-09-15
Scheduled daily dependency update on saturday
2018-09-15 17:40:49 +02:00
pyup-bot
f4d26961c8 Update ccxt from 1.17.291 to 1.17.294 2018-09-15 14:28:05 +02:00
Matthias
029a6798a4 Merge pull request #1209 from freqtrade/pyup-scheduled-update-2018-09-14
Scheduled daily dependency update on friday
2018-09-14 19:39:08 +02:00
pyup-bot
f5ba34addf Update ccxt from 1.17.283 to 1.17.291 2018-09-14 14:28:05 +02:00
Matthias
bcf47b29ed Merge pull request #1208 from freqtrade/pyup-scheduled-update-2018-09-13
Scheduled daily dependency update on thursday
2018-09-13 19:23:10 +02:00
pyup-bot
91c0e3640f Update ccxt from 1.17.276 to 1.17.283 2018-09-13 14:29:06 +02:00
Samuel Husso
fadf82dd32 Merge pull request #1205 from freqtrade/pyup-scheduled-update-2018-09-12
Scheduled daily dependency update on wednesday
2018-09-12 17:44:27 +03:00
pyup-bot
241b23e5d8 Update ccxt from 1.17.271 to 1.17.276 2018-09-12 14:28:06 +02:00
Matthias
c429eae6e4 Adjust remaining tests to _load_markets refactoring 2018-09-11 19:59:01 +02:00
Matthias
674bad2a4f Add and fix tests for load_markets 2018-09-11 19:46:47 +02:00
Matthias
14b7fc42fa Change returntype for _load_markets to dict 2018-09-11 19:46:31 +02:00
Matthias
14717b1701 Merge pull request #1203 from freqtrade/pyup-scheduled-update-2018-09-11
Scheduled daily dependency update on tuesday
2018-09-11 16:55:16 +02:00
pyup-bot
51ef137981 Update ccxt from 1.17.257 to 1.17.271 2018-09-11 14:27:07 +02:00
Matthias
f954efbd64 Adapt tests to not _load_markets 2018-09-10 20:19:28 +02:00
Matthias
0a29096794 Refactor load_market out of validate_pairs 2018-09-10 20:19:12 +02:00
Matthias
687dc78dbd Merge pull request #1202 from freqtrade/pyup-scheduled-update-2018-09-10
Scheduled daily dependency update on monday
2018-09-10 19:04:23 +02:00
pyup-bot
8aaf174578 Update ccxt from 1.17.250 to 1.17.257 2018-09-10 14:27:08 +02:00
Matthias
2660be9b13 Merge pull request #1201 from freqtrade/pyup-scheduled-update-2018-09-09
Scheduled daily dependency update on sunday
2018-09-09 15:47:09 +02:00
pyup-bot
65ad9cf741 Update ccxt from 1.17.242 to 1.17.250 2018-09-09 14:27:06 +02:00
Matthias
179bcf3907 Merge pull request #1101 from mishaker/ccxt-async
use ccxt async for ticker_history download
2018-09-09 08:39:57 +02:00
Samuel Husso
062eca19b8 Merge pull request #1199 from freqtrade/doc_ratelimit
Document ccxt_rate_limit
2018-09-08 16:06:59 +03:00
Samuel Husso
4692174677 Merge pull request #1200 from freqtrade/pyup-scheduled-update-2018-09-08
Scheduled daily dependency update on saturday
2018-09-08 16:06:35 +03:00
pyup-bot
65699f702e Update ccxt from 1.17.240 to 1.17.242 2018-09-08 14:27:07 +02:00
Matthias
e57be10772 Document ccxt_rate_limit 2018-09-08 13:01:33 +02:00
Samuel Husso
5ba6cfe406 Merge pull request #1195 from freqtrade/update_hyperopt_doc
explicitly ask for more ressources in hyperopt documentation
2018-09-07 15:56:47 +03:00
Samuel Husso
f0c7394bc8 Merge pull request #1197 from freqtrade/pyup-scheduled-update-2018-09-07
Scheduled daily dependency update on friday
2018-09-07 15:56:26 +03:00
pyup-bot
fb4f83b32c Update pytest from 3.7.4 to 3.8.0 2018-09-07 14:28:09 +02:00
pyup-bot
a49a60b4fa Update ccxt from 1.17.233 to 1.17.240 2018-09-07 14:28:07 +02:00
misagh
13ffd88053 merging develop into async. requirement.txt conflict resolved 2018-09-06 20:28:07 +02:00
Matthias
4e847f26bc explicitly ask for more ressources in hyperopt documentation 2018-09-06 20:12:16 +02:00
Matthias
0004b32411 Merge pull request #1194 from freqtrade/pyup-scheduled-update-2018-09-06
Scheduled daily dependency update on thursday
2018-09-06 19:51:42 +02:00
pyup-bot
4f583d61c8 Update ccxt from 1.17.231 to 1.17.233 2018-09-06 14:28:06 +02:00
Samuel Husso
3eb2e92d53 Merge pull request #1191 from freqtrade/pyup-scheduled-update-2018-09-05
Scheduled daily dependency update on wednesday
2018-09-05 16:01:27 +03:00
pyup-bot
a748c0794e Update ccxt from 1.17.229 to 1.17.231 2018-09-05 14:28:06 +02:00
Matthias
1682d6b365 Merge pull request #1188 from freqtrade/pyup-scheduled-update-2018-09-04
Scheduled daily dependency update on tuesday
2018-09-04 19:22:29 +02:00
pyup-bot
27ffce4c3f Update pytest-cov from 2.5.1 to 2.6.0 2018-09-04 14:28:08 +02:00
pyup-bot
d62f97dc3b Update ccxt from 1.17.223 to 1.17.229 2018-09-04 14:28:06 +02:00
Matthias
9c1cd4bee2 Merge pull request #1187 from freqtrade/pyup-scheduled-update-2018-09-03
Scheduled daily dependency update on monday
2018-09-03 19:15:03 +02:00
pyup-bot
754027efed Update ccxt from 1.17.222 to 1.17.223 2018-09-03 14:28:07 +02:00
Matthias
e9deb928f6 Fix bug when exchange result is empty 2018-09-02 19:15:23 +02:00
Matthias
6b74fb0893 Merge pull request #1119 from creslinux/ta_on_candle
ta_on_candle (not loop, with optional flag in config.json) Resubmitting - because GIT.
2018-09-02 17:01:21 +02:00
Samuel Husso
feb14990c2 Merge pull request #1186 from freqtrade/pyup-scheduled-update-2018-09-02
Scheduled daily dependency update on sunday
2018-09-02 16:10:26 +03:00
pyup-bot
3831f198e9 Update python-telegram-bot from 11.0.0 to 11.1.0 2018-09-02 14:28:07 +02:00
pyup-bot
adfd8c7f5c Update ccxt from 1.17.216 to 1.17.222 2018-09-02 14:28:06 +02:00
Matthias
3fd00c9a9c Merge branch 'develop' into ta_on_candle 2018-09-01 20:01:18 +02:00
Matthias
2ec5a536aa Fix comment location 2018-09-01 19:57:12 +02:00
Matthias
d35d3bb38c rename ta_on_candle to process_only_new_candles
be more expressive
2018-09-01 19:52:40 +02:00
Matthias
cb46aeb73c rename variable to be more expressive 2018-09-01 19:50:45 +02:00
Matthias
b8624e5909 Merge pull request #1183 from freqtrade/pyup-scheduled-update-2018-09-01
Scheduled daily dependency update on saturday
2018-09-01 19:27:15 +02:00
pyup-bot
fa5c8e4bb1 Update ccxt from 1.17.210 to 1.17.216 2018-09-01 14:28:06 +02:00
Samuel Husso
9945b97595 Merge pull request #1175 from freqtrade/doc/installation
installation documentation update
2018-08-31 23:05:12 +03:00
Matthias
17d6d92302 Merge pull request #1179 from freqtrade/pyup-scheduled-update-2018-08-30
Scheduled daily dependency update on thursday
2018-08-30 19:10:00 +02:00
pyup-bot
9560cb8056 Update pytest from 3.7.3 to 3.7.4 2018-08-30 14:28:10 +02:00
pyup-bot
3ed97fe5e8 Update python-telegram-bot from 10.1.0 to 11.0.0 2018-08-30 14:28:08 +02:00
pyup-bot
35c5d4f580 Update ccxt from 1.17.205 to 1.17.210 2018-08-30 14:28:07 +02:00
Matthias
a1bd30aa60 Fix documentation string 2018-08-29 19:59:25 +02:00
Matthias
ffd4469c1d fix typo, refresh_tickers does not need a return value 2018-08-29 19:56:38 +02:00
Matthias
54ddd908e6 Merge branch 'develop' into ccxt-async 2018-08-29 19:43:09 +02:00
Matthias
d41f0667b8 Merge pull request #1125 from nullart2/order-book
Order Book with tests
2018-08-29 19:36:01 +02:00
Matthias
9f8e68ce02 Merge branch 'develop' into order-book 2018-08-29 19:32:44 +02:00
Matthias
f7b67cec5b Fix missing docstring 2018-08-29 19:16:41 +02:00
Matthias
e14e7d9b8a Merge pull request #1177 from freqtrade/pyup-scheduled-update-2018-08-29
Scheduled daily dependency update on wednesday
2018-08-29 17:04:41 +02:00
pyup-bot
b659ec00ee Update ccxt from 1.17.199 to 1.17.205 2018-08-29 14:28:07 +02:00
Nullart2
b6b89a464f move order_book config out of experimental 2018-08-29 17:38:43 +08:00
Matthias
c9ee528050 Add section about raspberry / conda to install.md 2018-08-28 22:06:46 +02:00
Matthias
9bce6c5f48 Add error-section for windows 2018-08-28 19:30:26 +02:00
Matthias
cdfff57403 Merge pull request #1174 from freqtrade/pyup-scheduled-update-2018-08-28
Scheduled daily dependency update on tuesday
2018-08-28 19:11:09 +02:00
pyup-bot
19628d317a Update ccxt from 1.17.194 to 1.17.199 2018-08-28 14:28:06 +02:00
Matthias
32ae344e59 Merge pull request #1172 from freqtrade/pyup-scheduled-update-2018-08-27
Scheduled daily dependency update on monday
2018-08-27 15:51:22 +02:00
pyup-bot
c99ff78f2f Update pytest from 3.7.2 to 3.7.3 2018-08-27 14:28:07 +02:00
pyup-bot
188cfc435d Update ccxt from 1.17.188 to 1.17.194 2018-08-27 14:28:05 +02:00
Matthias
1a9c085f10 Restructure install documentation 2018-08-26 20:09:12 +02:00
Samuel Husso
eefc5349c8 Merge pull request #1171 from freqtrade/pyup-scheduled-update-2018-08-26
Scheduled daily dependency update on sunday
2018-08-26 18:55:47 +03:00
pyup-bot
fe169483ed Update ccxt from 1.17.184 to 1.17.188 2018-08-26 14:28:07 +02:00
nullart2
4dfaf1d284 Merge pull request #5 from xmatthias/order_book_xmatt
fix some test mockings in orderbook pr
2018-08-26 20:01:42 +08:00
Matthias
c5efcace47 change pip3.6 to pip3 2018-08-26 12:49:39 +02:00
Samuel Husso
c770eae70b Merge pull request #1168 from freqtrade/pyup-scheduled-update-2018-08-25
Scheduled daily dependency update on saturday
2018-08-25 17:06:58 +03:00
pyup-bot
2ee1a2d851 Update ccxt from 1.17.176 to 1.17.184 2018-08-25 14:28:06 +02:00
Matthias
42587741dd mock exchange to avoid random failures 2018-08-25 13:21:10 +02:00
Matthias
a489a044ad Mock Exchange results to avoid random test-failures 2018-08-25 13:17:07 +02:00
Matthias
1d0802192d Merge pull request #1167 from freqtrade/pyup-scheduled-update-2018-08-24
Scheduled daily dependency update on friday
2018-08-24 15:36:33 +02:00
pyup-bot
ab628c1381 Update ccxt from 1.17.170 to 1.17.176 2018-08-24 14:28:06 +02:00
Matthias
a37802e21c Merge pull request #1165 from freqtrade/pyup-scheduled-update-2018-08-23
Scheduled daily dependency update on thursday
2018-08-23 16:14:14 +02:00
pyup-bot
8c0e33753e Update ccxt from 1.17.163 to 1.17.170 2018-08-23 14:28:07 +02:00
Matthias
cac7e2c745 Merge pull request #1164 from freqtrade/pyup-scheduled-update-2018-08-22
Scheduled daily dependency update on wednesday
2018-08-22 19:29:07 +02:00
pyup-bot
ebc072396b Update numpy from 1.15.0 to 1.15.1 2018-08-22 14:28:09 +02:00
pyup-bot
4508349d07 Update ccxt from 1.17.157 to 1.17.163 2018-08-22 14:28:07 +02:00
Samuel Husso
7376a0d538 Merge pull request #1131 from freqtrade/parametrize_outdated_ticker
parametrize outdated_offset to simplify sandbox usage
2018-08-22 07:02:38 +03:00
Samuel Husso
36e0e652f0 Merge pull request #1135 from freqtrade/fix/rpc_balance_vtho
Fix /balance rpc call if coin is not properly listed
2018-08-22 07:01:40 +03:00
Samuel Husso
5e4ae46b3c Merge pull request #1163 from freqtrade/remove_amount_to_lots
remove amount_to_lots (deprecated / removed)
2018-08-22 07:01:09 +03:00
Misagh
66d52c1236 Merge pull request #4 from xmatthias/ccxt_async_retrier
Add async retrier
2018-08-21 19:55:30 +02:00
Matthias
6e90d482ef remove amount_to_lots (deprecated / removed)
was removed from ccxt in
527f082e59
2018-08-21 19:08:21 +02:00
Samuel Husso
37bb6ac57b Merge pull request #1162 from freqtrade/pyup-scheduled-update-2018-08-21
Scheduled daily dependency update on tuesday
2018-08-21 15:42:57 +03:00
pyup-bot
8a844488d4 Update sqlalchemy from 1.2.10 to 1.2.11 2018-08-21 14:28:08 +02:00
pyup-bot
e5707b8a2c Update ccxt from 1.17.152 to 1.17.157 2018-08-21 14:28:06 +02:00
Matthias
8f41e0e190 Use setting in 'exchange' dict 2018-08-20 20:01:57 +02:00
Samuel Husso
4bf0542204 Merge pull request #1161 from freqtrade/pyup-scheduled-update-2018-08-20
Scheduled daily dependency update on monday
2018-08-20 19:07:03 +03:00
pyup-bot
43f73c5aec Update ccxt from 1.17.146 to 1.17.152 2018-08-20 14:28:06 +02:00
Matthias
a077955efa update json.load to json_load - followup to #1142 2018-08-19 19:58:07 +02:00
Matthias
0674c3e8f0 Merge pull request #1142 from freqtrade/ujson-loader
backtesting: try to load data with ujson if it exists
2018-08-19 19:53:38 +02:00
Matthias
6d1c82a5fa Remove last refreence to get_candle_history 2018-08-19 19:50:14 +02:00
Matthias
de0f3e43bf remove unused mocks 2018-08-19 19:49:39 +02:00
Matthias
694b8be32f Move variables from class to instance 2018-08-19 19:49:02 +02:00
Matthias
9403248e4d have plot-script use async ticker-refresh 2018-08-19 19:48:24 +02:00
Samuel Husso
c955c7c494 Merge pull request #1160 from freqtrade/pyup-scheduled-update-2018-08-19
Scheduled daily dependency update on sunday
2018-08-19 18:14:46 +03:00
pyup-bot
5a0876704a Update pytest from 3.7.1 to 3.7.2 2018-08-19 14:28:07 +02:00
pyup-bot
97e9a44fd2 Update ccxt from 1.17.139 to 1.17.146 2018-08-19 14:28:06 +02:00
Matthias
088c54b88c remove unnecessary function 2018-08-19 09:17:17 +02:00
Matthias
d722c12109 fix bug in async download script 2018-08-18 21:08:59 +02:00
Matthias
d556f669b0 Add async retrier 2018-08-18 21:05:38 +02:00
Matthias
66255b8c61 Merge pull request #1159 from freqtrade/pyup-scheduled-update-2018-08-18
Scheduled daily dependency update on saturday
2018-08-18 17:24:38 +02:00
pyup-bot
bc22320f77 Update ccxt from 1.17.134 to 1.17.139 2018-08-18 14:27:07 +02:00
Samuel Husso
64781643d3 Merge pull request #1157 from freqtrade/pyup-scheduled-update-2018-08-17
Scheduled daily dependency update on friday
2018-08-17 18:55:04 +03:00
pyup-bot
56188f2f67 Update ccxt from 1.17.132 to 1.17.134 2018-08-17 14:27:07 +02:00
Samuel Husso
eb4bc66443 Merge pull request #1156 from freqtrade/add_min_roi_test
Add explicit test on handling min_roi_reached
2018-08-17 09:59:10 +03:00
Matthias
d1c5eebff2 Add explicit test on handling min_roi_reached 2018-08-17 06:50:36 +02:00
Samuel Husso
98240e0e48 Merge pull request #1154 from freqtrade/min_roi_output
Output min-roi setting when overwriting from config
2018-08-16 20:18:49 +03:00
Samuel Husso
0750d356a1 Merge pull request #1141 from freqtrade/fix/python3.7
fix running freqtrade on python3.7
2018-08-16 20:17:24 +03:00
Matthias
f57bf8f269 Merge pull request #1155 from freqtrade/pyup-scheduled-update-2018-08-16
Scheduled daily dependency update on thursday
2018-08-16 14:36:53 +02:00
pyup-bot
dc41a19f99 Update ccxt from 1.17.126 to 1.17.132 2018-08-16 14:27:06 +02:00
Matthias
16fa877b67 Remove verbosity of trying backup tables - properly log if
databasemigration happened
2018-08-16 13:15:46 +02:00
Matthias
ff8ed564f1 Refactor refresh_pairs to exchange and fix tests 2018-08-16 12:15:09 +02:00
misagh
e6e2799f03 Keeping cached Klines only in exchange and renaming _cached_klines to
klines.
2018-08-16 11:37:31 +02:00
Matthias
4a8c120926 Output min-roi setting when overwriting from config 2018-08-16 11:35:41 +02:00
Samuel Husso
aa10c6e6fe master to RELEASE 0.17.1 2018-08-16 08:12:36 +03:00
misagh
a2d9126917 Merge branch 'develop' into ccxt-async 2018-08-15 15:09:35 +02:00
Samuel Husso
e02f964e3a Merge pull request #1152 from freqtrade/pyup-scheduled-update-2018-08-15
Scheduled daily dependency update on wednesday
2018-08-15 15:46:24 +03:00
pyup-bot
be373e7563 Update ccxt from 1.17.122 to 1.17.126 2018-08-15 14:27:06 +02:00
Matthias
baeffee80d Replace time.time with arrow.utcnow().timestamp
arrow is imported already
2018-08-15 13:26:01 +02:00
Matthias
76914c2c07 remove todo comment as this is actually done 2018-08-15 12:57:27 +02:00
Matthias
ca6594cd24 remove comment, add docstring 2018-08-15 12:49:39 +02:00
Matthias
d007ac4b96 check version explicitly, use "python" in venv 2018-08-15 08:37:20 +02:00
Janne Sinivirta
6e2a2abe80 Merge pull request #1151 from freqtrade/version-bump
Push develop as 0.17.2
2018-08-15 08:26:43 +03:00
Samuel Husso
dd7f540e5a Push develop as 0.17.2 2018-08-15 08:25:04 +03:00
Samuel Husso
78d1a677d7 Merge pull request #1140 from freqtrade/update_plotly
update plotly dependency
2018-08-15 08:18:06 +03:00
Matthias
2999588ea7 Merge pull request #1150 from nullart2/informative_startup
Informative startup
2018-08-15 06:43:51 +02:00
Nullart2
1edbc494ee refactor 2018-08-15 12:37:30 +08:00
Nullart2
b34aa46181 additional tests 2018-08-15 12:05:56 +08:00
Nullart2
48e218d6c0 test_talib fix 2018-08-15 11:01:59 +08:00
Nullart2
2bc7a668a3 informative startup 2018-08-15 10:39:32 +08:00
nullart2
8b9f1cadaa Merge pull request #2 from freqtrade/develop
dev update
2018-08-15 09:59:42 +08:00
Matthias
3aa210cf93 Add test for get_history 2018-08-14 20:53:58 +02:00
Matthias
e37cb49dc2 Ad test for async_load_markets 2018-08-14 20:42:13 +02:00
Matthias
67cbbc86f2 Add test for exception 2018-08-14 20:35:12 +02:00
Matthias
37e504610a refactor private method - improve some async tests 2018-08-14 20:33:03 +02:00
Matthias
8528143ffa Properly close async exchange as requested by ccxt 2018-08-14 19:52:09 +02:00
Matthias
69cc6aa958 Add test to async 2018-08-14 16:02:03 +02:00
misagh
a6b69da391 Merge branch 'develop' into ccxt-async 2018-08-14 15:30:34 +02:00
Matthias
05cfbde8fc Merge pull request #1146 from freqtrade/pyup-scheduled-update-2018-08-14
Scheduled daily dependency update on tuesday
2018-08-14 14:40:58 +02:00
pyup-bot
04878da66b Update ccxt from 1.17.118 to 1.17.122 2018-08-14 14:27:07 +02:00
misagh
0b44dda7b7 Merge pull request #3 from xmatthias/ccxt-async_xmatt
ccxt async download
2018-08-14 13:21:13 +02:00
Nullart2
78610bb47f mock order_book and additional test 2018-08-14 18:12:44 +08:00
Matthias
50494858f1 Merge pull request #1144 from freqtrade/pyup-scheduled-update-2018-08-13
Scheduled daily dependency update on monday
2018-08-13 14:42:09 +02:00
pyup-bot
eca8682528 Update ccxt from 1.17.113 to 1.17.118 2018-08-13 14:26:06 +02:00
Matthias
a488734efa Merge pull request #1143 from freqtrade/pyup-scheduled-update-2018-08-12
Scheduled daily dependency update on sunday
2018-08-12 19:03:17 +02:00
pyup-bot
2e7837976d Update ccxt from 1.17.106 to 1.17.113 2018-08-12 14:26:06 +02:00
Matthias
a0bc17d1ef Update dockerfile to 3.7.0 2018-08-12 13:59:50 +02:00
Matthias
2b37c1ff0e Merge branch 'develop' into ujson-loader 2018-08-12 13:11:40 +02:00
Matthias
7d72e364aa Remove broken ujson loading - replace with variable-based fix 2018-08-12 13:08:10 +02:00
creslin
bd61478367 Merge pull request #2 from xmatthias/ta_on_candle_xmatt
Ta on candle xmatt
2018-08-12 10:07:58 +00:00
Matthias
f7afd9a5ff update setup.sh to support 3.7 2018-08-12 10:37:10 +02:00
Matthias
7f6f5791ea update plotly dependency 2018-08-12 10:25:19 +02:00
Matthias
e3e79a55fa Fix _abc_data pickle error in 3.7 2018-08-12 10:16:51 +02:00
Matthias
e73331b9b6 Merge pull request #1124 from berlinguyinca/database_tuning
Database tuning
2018-08-12 09:45:48 +02:00
Matthias
ffa47151ee Flake8 fix 2018-08-12 09:30:12 +02:00
Matthias
5f8ec82319 Revert "updated dockerfile and requirements"
This reverts commit 2cfa3b7607.
2018-08-12 09:18:30 +02:00
Matthias
3ad6ee6b2c Merge pull request #1139 from freqtrade/pyup-scheduled-update-2018-08-11
Scheduled daily dependency update on saturday
2018-08-11 19:27:52 +02:00
pyup-bot
5bec389e85 Update ccxt from 1.17.94 to 1.17.106 2018-08-11 14:26:06 +02:00
Matthias
88e85e8d33 fix tests - move load_async_markets call to validate_pairs 2018-08-10 13:11:04 +02:00
Matthias
fce071843d Move async-load to seperate function 2018-08-10 13:04:43 +02:00
Matthias
a852d2ff32 default since_ms to 30 days if no timerange is given 2018-08-10 11:15:02 +02:00
Matthias
a107c4c7b4 Download using asyncio 2018-08-10 11:08:28 +02:00
Matthias
74d6816a1a Fix some comments 2018-08-10 11:00:07 +02:00
Matthias
e34f2abc3a Add some typehints 2018-08-10 09:58:04 +02:00
Matthias
8a0fc888d6 log if using cached data 2018-08-10 09:48:54 +02:00
Matthias
36f05af79a sort fetch_olvhc result, refactor some
* add exception for since_ms - if this is set it should always download
2018-08-10 09:44:15 +02:00
Matthias
e654b76bc8 Fix async test 2018-08-10 09:44:03 +02:00
Matthias
56768f1a61 Flake8 in tests ... 2018-08-09 20:17:55 +02:00
Matthias
b008649d79 Remove unnecessary quote escaping 2018-08-09 20:13:07 +02:00
Matthias
3b2f161573 Add test for ta_on_candle override 2018-08-09 20:12:45 +02:00
Matthias
df960241bd Add log-message for skipped candle and tests 2018-08-09 20:07:01 +02:00
Matthias
4ece5d6d7a Add tests for ta_on_candle 2018-08-09 20:02:24 +02:00
Matthias
e36067afd3 refactor candle_seen to private 2018-08-09 19:58:47 +02:00
Matthias
c4e43039f2 Allow control from strategy 2018-08-09 19:24:00 +02:00
Matthias
853374d156 Merge pull request #1136 from freqtrade/pyup-scheduled-update-2018-08-09
Scheduled daily dependency update on thursday
2018-08-09 19:15:47 +02:00
pyup-bot
1bcd4333fc Update ccxt from 1.17.86 to 1.17.94 2018-08-09 14:26:06 +02:00
Matthias
029d61b8c5 Add ta_on_candle descripton to support strategy 2018-08-09 13:12:12 +02:00
misagh
280ead7bdb Merge branch 'develop' into ccxt-async 2018-08-09 13:04:01 +02:00
Matthias
98730939d4 Refactor to use a plain dict
* check config-setting first - avoids any call to "candle_seen"
eventually
2018-08-09 13:02:41 +02:00
Matthias
d1306a2177 Fix failing tests when metadata in analyze_ticker is actually used 2018-08-09 13:01:57 +02:00
misagh
cb26085229 Moving should_not_update logic to async function per pair. if there is
no new candle, async function will just return the last cached candle
locally and doesn’t hit the API
2018-08-09 12:47:26 +02:00
Matthias
ed4771bf6e Merge pull request #1130 from freqtrade/fix_metadatatests
Fix failing tests when metadata in `analyze_ticker` is actually used
2018-08-09 12:46:35 +02:00
misagh
cef09f49a6 wait for markets to be loaded before looping in symbols. 2018-08-09 11:51:38 +02:00
Matthias
e1921c8849 Fix bug causing /balance to fail 2018-08-08 22:00:39 +02:00
Matthias
3c451e0677 Add test for bugreport #1111 2018-08-08 21:54:52 +02:00
Matthias
636ae1dcd8 Merge pull request #1134 from freqtrade/pyup-scheduled-update-2018-08-08
Scheduled daily dependency update on wednesday
2018-08-08 19:19:39 +02:00
pyup-bot
4d03fc213f Update ccxt from 1.17.84 to 1.17.86 2018-08-08 14:26:07 +02:00
Samuel Husso
863110422a Merge pull request #1132 from freqtrade/pyup-scheduled-update-2018-08-07
Scheduled daily dependency update on tuesday
2018-08-07 17:54:11 +03:00
pyup-bot
3d94720be9 Update ccxt from 1.17.81 to 1.17.84 2018-08-07 14:26:07 +02:00
Nullart2
c9c0e108ab refactor 2018-08-07 18:29:37 +08:00
Matthias
c9580b31d0 parametrize outdated_offset to simplify sandbox usage 2018-08-07 09:25:21 +02:00
Matthias
255f303850 Fix tests and flake8 2018-08-07 08:56:06 +02:00
Matthias
131d268721 Fix failing tests when metadata in analyze_ticker is actually used 2018-08-06 19:15:30 +02:00
Matthias
eca5c6f389 Merge pull request #1129 from freqtrade/pyup-scheduled-update-2018-08-06
Scheduled daily dependency update on monday
2018-08-06 15:29:56 +02:00
pyup-bot
bc62f626c5 Update ccxt from 1.17.78 to 1.17.81 2018-08-06 14:26:06 +02:00
Samuel Husso
199bd7bc50 Merge pull request #1123 from freqtrade/fix-db_migration
Fix db migration
2018-08-06 12:00:22 +03:00
Janne Sinivirta
8fc0f6ecec Merge pull request #1128 from Axel-CH/fix-talib-prescision
fix talib bug on bollinger bands and other indicators
2018-08-06 08:35:35 +03:00
Axel Cherubin
65f7b75c34 fix flake8 issue 2018-08-05 17:52:06 -04:00
Axel Cherubin
848ecb91bb remove unnecessary seb command 2018-08-05 17:28:53 -04:00
Axel Cherubin
a5554604e0 add sed command in doc, fix travis error 2018-08-05 16:59:18 -04:00
Axel Cherubin
0b825e96aa fix talib bug on bollinger bands and other indicators when working on small assets, rise talib prescision and add test associated 2018-08-05 16:08:49 -04:00
Matthias
a2730cd86e Merge pull request #1126 from freqtrade/pyup-scheduled-update-2018-08-05
Scheduled daily dependency update on sunday
2018-08-05 19:18:11 +02:00
Nullart2
1309c2b14f tests update 2018-08-05 22:56:14 +08:00
Nullart2
7143b64fb7 tests for coverage 2018-08-05 22:41:58 +08:00
Nullart2
26d591ea43 mypy fix 2018-08-05 21:08:07 +08:00
pyup-bot
ba4de4137e Update pandas from 0.23.3 to 0.23.4 2018-08-05 14:26:08 +02:00
pyup-bot
be9436b2a6 Update ccxt from 1.17.73 to 1.17.78 2018-08-05 14:26:07 +02:00
Nullart2
4a9bf78770 Order Book with tests 2018-08-05 12:41:06 +08:00
Matthias
d73d0a5253 Fix database migration 2018-08-04 20:22:45 +02:00
Matthias
ea506b05c6 Add test for failing database migration 2018-08-04 20:22:16 +02:00
Samuel Husso
6ef14677de Merge pull request #1122 from freqtrade/pyup-scheduled-update-2018-08-04
Scheduled daily dependency update on saturday
2018-08-04 19:55:20 +03:00
pyup-bot
721341e412 Update ccxt from 1.17.66 to 1.17.73 2018-08-04 14:26:05 +02:00
misagh
3ce4d20ab9 using constants instead of stripping the string 2018-08-04 13:04:16 +02:00
misagh
af93b18475 Do not refresh candles on "process_throttle_secs" but on intervals 2018-08-03 18:10:03 +02:00
Samuel Husso
a586a7526e Merge pull request #1120 from freqtrade/pyup-scheduled-update-2018-08-03
Scheduled daily dependency update on friday
2018-08-03 16:11:14 +03:00
misagh
3987a8aeb8 Merge branch 'ccxt-async' of https://github.com/misaghshakeri/freqtrade into ccxt-async 2018-08-03 14:50:11 +02:00
misagh
59b9a6d94d Break the loop as soon as one buy signal is found. 2018-08-03 14:49:55 +02:00
pyup-bot
b963b95ee9 Update pytest from 3.7.0 to 3.7.1 2018-08-03 14:26:07 +02:00
pyup-bot
3037d85529 Update ccxt from 1.17.63 to 1.17.66 2018-08-03 14:26:06 +02:00
creslin
10ab6c7ffa Removed unneeded property code 2018-08-03 09:14:16 +00:00
creslin
71b0e15182 updated configuration.md 2018-08-03 08:45:24 +00:00
creslin
1fef384bba flake 8 2018-08-03 08:40:16 +00:00
creslin
d2a728cebd flake 8 2018-08-03 08:38:13 +00:00
creslin
6b3e8dcc33 holds a dict of each pair last seen.
to correctly manage the last seen of a pair.
2018-08-03 08:33:37 +00:00
creslin
c38d94df2d Resubmitting - because GIT.
This is the last cut that was in #1117 before i closed that PR

This PR allows a user to set the flag "ta_on_candle" in their config.json

This will change the behaviour of the the bot to only process indicators
when there is a new candle to be processed for that pair.

The test is made up of "last dataframe row date + pair" is different to
last_seen OR  ta_on_candle is not True
2018-08-03 07:33:34 +00:00
Gert Wohlgemuth
2cfa3b7607 updated dockerfile and requirements 2018-08-02 17:08:14 -07:00
Gert
85c73ea850 added index 2018-08-02 16:39:13 -07:00
Matthias
337d9174d9 Flake8 fixes 2018-08-02 20:11:27 +02:00
Matthias
80a1c6ea64 Merge pull request #1106 from creslinux/xbt
XBT missing as a market symbol for BTC in constants
2018-08-02 20:07:25 +02:00
misagh
05ca78d2a3 ticker_history changed to candle_history naming 2018-08-02 17:10:38 +02:00
misagh
2ec2f1abce async branch updated to reflect develop branch changes 2018-08-02 16:48:21 +02:00
misagh
7dc440b874 Merge pull request #2 from xmatthias/ccxt-async-xmatt
Ccxt async xmatt
2018-08-02 16:33:02 +02:00
Matthias
ea72af7ce4 Merge pull request #1118 from freqtrade/pyup-scheduled-update-2018-08-02
Scheduled daily dependency update on thursday
2018-08-02 14:44:53 +02:00
pyup-bot
145008421f Update ccxt from 1.17.60 to 1.17.63 2018-08-02 14:26:07 +02:00
Samuel Husso
398c61786a Merge pull request #1116 from creslinux/script_get_market_pairs
Script to get market pairs
2018-08-02 13:29:42 +03:00
Matthias
00b81e3f0d fix readme.md spelling 2018-08-02 13:27:37 +03:00
Matthias
0fc4a7910d Add note to readme for binance users 2018-08-02 13:27:37 +03:00
creslin
7f4472ad77 As requested in issue #1111
A python script to return

 - all exchanges supported by CCXT
 - all markets on a exchange

 Invoked as `python get_market_pairs.py` it will list exchanges
 Invoked as `python get_market_pairs binance` it will list all markets on binance
2018-08-02 10:10:44 +00:00
Janne Sinivirta
e282d57a91 fix broken test 2018-08-02 12:57:47 +03:00
Janne Sinivirta
3a5b435dfa Merge pull request #1089 from freqtrade/feat/backtest_multi_strat
Allow multi strategy backtest without data reload
2018-08-02 12:35:47 +03:00
Janne Sinivirta
17d78b7807 Merge pull request #1115 from creslinux/candlesnottickers
renamed/refactored get_ticker_history to get_candle_history to stop confusion
2018-08-02 12:33:09 +03:00
creslin
1f97d0d78b fix 2018-08-02 09:15:02 +00:00
creslin
a741f1144a missing __init__.py 2018-08-02 08:58:04 +00:00
creslin
f619cd1d2a renamed/refactored get_ticker_history to get_candle_history
as it does not fetch any ticker data only candles
and is causing confusion when developer are talking about candles /tickers
incorreclty.

OHLCV < candles and Tickers are two seperate datafeeds from the exchange
2018-08-02 08:45:28 +00:00
Matthias
9c08cdc81d Fix typehints 2018-08-01 21:58:32 +02:00
Matthias
915160f21f Add tests for tickers-history 2018-08-01 21:44:02 +02:00
Matthias
c466a028e0 Add a first async test 2018-08-01 21:40:54 +02:00
Matthias
29dcd2ea43 Merge pull request #1108 from freqtrade/pyup-scheduled-update-2018-08-01
Scheduled daily dependency update on wednesday
2018-08-01 15:38:23 +02:00
pyup-bot
f7f75b4b04 Update ccxt from 1.17.56 to 1.17.60 2018-08-01 14:26:05 +02:00
Matthias
7458aa438c Merge pull request #982 from berlinguyinca/BASE64
integrated BASE64 encoded strategy loading
2018-08-01 09:00:12 +02:00
creslin
36f91fcdf5 XBT missing as a market symbol for BTC in constants 2018-08-01 06:03:34 +00:00
Matthias
5b8ee214f9 Adapt to pair_to_strat methology 2018-08-01 07:28:12 +02:00
Matthias
038e97667f Merge branch 'develop' into BASE64 2018-08-01 07:26:13 +02:00
misagh
b47c5f1d9a Merge pull request #1 from xmatthias/ccxt-async-xmatt
some fixes and improvements hopefully
2018-07-31 21:21:45 +02:00
Matthias
40ee86b357 Adapt after rebase 2018-07-31 21:08:03 +02:00
Matthias
76fbb89a03 use print for backtest results to avoid odd newline-handling 2018-07-31 21:04:03 +02:00
Matthias
c648e2acfc Adjust documentation to strategy table 2018-07-31 21:04:03 +02:00
Matthias
765d1c769c Add test for stratgy summary table 2018-07-31 21:04:03 +02:00
Matthias
028589abd2 Add strategy summary table 2018-07-31 21:04:03 +02:00
Matthias
5125076f5d Fix typo 2018-07-31 21:04:03 +02:00
Matthias
4ea6780153 Update documentation with --strategy-list 2018-07-31 21:04:03 +02:00
Matthias
a8b55b8989 Add test for strategy-name injection 2018-07-31 21:04:03 +02:00
Matthias
a57a2f4a75 Store backtest-result in different vars 2018-07-31 21:04:03 +02:00
Matthias
bd3563df67 Add test for new functionality 2018-07-31 21:04:03 +02:00
Matthias
644f729aea Refactor strategy loading to __init__ 2018-07-31 21:04:03 +02:00
Matthias
5f2e92ec5c Refactor backtesting 2018-07-31 21:04:03 +02:00
Matthias
65aaa3dffd Extract backtest strategy setting 2018-07-31 21:04:03 +02:00
Matthias
9a42aac0f2 Add testcase for --strategylist 2018-07-31 21:04:03 +02:00
Matthias
56046b3cb3 Add strategylist option to backtesting 2018-07-31 21:04:03 +02:00
Matthias
e7d0439741 Add new arguments 2018-07-31 21:03:17 +02:00
Matthias
136442245c Add todo's and dockstring 2018-07-31 21:02:04 +02:00
Matthias
12417cc303 fix tests 2018-07-31 20:54:51 +02:00
Matthias
52065178e1 use .get all the time 2018-07-31 20:53:32 +02:00
Matthias
b45d465ed8 init _klines properly 2018-07-31 20:50:59 +02:00
Matthias
31870abd25 Refactor async-refresh to it's own function 2018-07-31 20:43:32 +02:00
Matthias
a486b1d01c Use Dict instead of tuplelist, run in _process 2018-07-31 20:25:10 +02:00
Matthias
e38e0e60e1 Merge pull request #1103 from misaghshakeri/ccxt_ratelimit_configurable
Initializing CCXT with rate_limit parameter optional (default to true) [EDITED]
2018-07-31 19:46:28 +02:00
misagh
74fa4ddca4 CCXT rate limit config default to => true
+ adding config to config_full.json.example
2018-07-31 16:54:02 +02:00
Matthias
66a0986496 Merge pull request #1102 from freqtrade/pyup-scheduled-update-2018-07-31
Scheduled daily dependency update on tuesday
2018-07-31 14:39:48 +02:00
pyup-bot
72480188b7 Update pytest from 3.6.4 to 3.7.0 2018-07-31 14:25:07 +02:00
pyup-bot
ab4343b7c0 Update ccxt from 1.17.49 to 1.17.56 2018-07-31 14:25:06 +02:00
misagh
be1298dbd2 Initializing CCXT with rate_limit parameter optional (default to false) 2018-07-31 14:19:16 +02:00
misagh
154e4569d7 Merge branch 'develop' into ccxt-async 2018-07-31 12:48:12 +02:00
misagh
c8f125dbb9 ccxt async POC 2018-07-31 12:47:32 +02:00
Janne Sinivirta
1044d15b17 Merge pull request #1096 from freqtrade/cleaner-tests
Cleaning unit tests, first set
2018-07-31 08:22:33 +03:00
Janne Sinivirta
2d7ef30185 Merge pull request #1093 from freqtrade/fix/talib-install
install numpy before ta-lib to fix build errors
2018-07-31 08:19:35 +03:00
Gert
b83487cc36 added required changes 2018-07-30 13:00:08 -07:00
Matthias
d048f3ce6d Merge pull request #1078 from creslinux/sandbox2
Allow sandbox API use on exchanges
2018-07-30 20:23:28 +02:00
Matthias
5a55cd25ff Merge branch 'develop' into sandbox2 2018-07-30 20:18:48 +02:00
Janne Sinivirta
f85cc422a3 Merge branch 'develop' into cleaner-tests 2018-07-30 21:08:55 +03:00
Janne Sinivirta
155e134f50 Merge pull request #1097 from creslinux/gdax3
Enable GDAX support by rounding amount/rate (with unit tests)
2018-07-30 21:04:26 +03:00
Janne Sinivirta
81cf7229be Merge pull request #1044 from freqtrade/pair_to_strat
pair to strategy enhancement
2018-07-30 20:18:46 +03:00
creslin
fe27ca63b4 Update test_exchange.py 2018-07-30 17:08:33 +00:00
creslinux
012fe94333 Recommitted as new branch with unit tests - GIT screwd me on the last PR 2018-07-30 16:49:58 +00:00
Matthias
075a42d615 Merge pull request #1095 from freqtrade/pyup-scheduled-update-2018-07-30
Scheduled daily dependency update on monday
2018-07-30 14:53:24 +02:00
Janne Sinivirta
8b8d3f3b75 default_conf is function-scoped fixture, no need to deepcopy it 2018-07-30 15:41:02 +03:00
pyup-bot
3ecc502d86 Update ccxt from 1.17.45 to 1.17.49 2018-07-30 14:24:06 +02:00
Janne Sinivirta
67d1693901 avoid validating default_conf hundreds of times 2018-07-30 14:57:51 +03:00
Janne Sinivirta
3083e5d2be use pytest fixture properly in test_hyperopt 2018-07-30 13:26:54 +03:00
Janne Sinivirta
affdeb8fd8 rename func to throttled_func 2018-07-30 12:58:29 +03:00
Janne Sinivirta
fb80964b69 freqtradebot tests don't need to mock coinmarketcap anymore 2018-07-30 12:58:29 +03:00
Janne Sinivirta
1c20ef873d remove parens 2018-07-30 12:09:07 +03:00
Janne Sinivirta
df53e912f0 fix one more test that was missing mock and needed internet 2018-07-30 12:09:07 +03:00
Janne Sinivirta
e242842805 remove more useless docstrings from tests 2018-07-30 12:09:07 +03:00
Matthias
2401fa15d2 Change missed calls to advise_* functions 2018-07-29 21:07:21 +02:00
Matthias
787d6042de Switch from pair(str) to metadata(dict) 2018-07-29 20:56:23 +02:00
Matthias
941879dc19 revert docs to use populate_* functions 2018-07-29 20:55:40 +02:00
Matthias
82680ac6aa improve docstrings for strategy 2018-07-29 20:55:40 +02:00
Matthias
5fbce13830 update hyperopt to use new methods 2018-07-29 20:55:40 +02:00
Matthias
39cf0decce don't use __annotate__
it is only present when typehints are used which cannot be guaranteed
for userdefined classes
2018-07-29 20:55:40 +02:00
Matthias
f286ba6b87 overload populate_indicators to work with and without pair argumen
all while not breaking users strategies
2018-07-29 20:55:40 +02:00
Matthias
98665dcef4 revert inadvertent wihtespace changes 2018-07-29 20:55:37 +02:00
Matthias
cf83416d69 update script to use new method 2018-07-29 20:55:37 +02:00
Matthias
791c5ff071 update comments to explain what advise methods do 2018-07-29 20:55:37 +02:00
Matthias
8a9c54ed61 use new methods 2018-07-29 20:55:37 +02:00
Matthias
18b8f20f1c fix small test bug 2018-07-29 20:55:37 +02:00
Matthias
f12167f0dc Fix backtesting test 2018-07-29 20:55:37 +02:00
Matthias
df8700ead0 Adapt after merge from develop 2018-07-29 20:55:37 +02:00
Matthias
0eff6719c2 improve tests for legacy-strategy loading 2018-07-29 20:55:37 +02:00
Matthias
aa772c28ad Add tests for advise_indicator methods 2018-07-29 20:55:37 +02:00
Matthias
4ebd706cb8 improve comments 2018-07-29 20:55:32 +02:00
Matthias
fa48b8a535 Update documentation with advise-* methods 2018-07-29 20:55:32 +02:00
Matthias
c9a97bccb7 Add tests for deprecation 2018-07-29 20:55:32 +02:00
Matthias
2f905cb696 Update test-strategy with new methods 2018-07-29 20:55:06 +02:00
Matthias
7300c0a0fe remove @abstractmethod as this method may not be present in new
strategies
2018-07-29 20:55:06 +02:00
Gert Wohlgemuth
921f645623 fixing tests... 2018-07-29 20:55:06 +02:00
Gert Wohlgemuth
0dcaa82c3b fixed test? 2018-07-29 20:55:06 +02:00
Gert Wohlgemuth
3dd7d209e9 more test fixes 2018-07-29 20:55:06 +02:00
Gert Wohlgemuth
abc55a6e6b fixing? hyperopt 2018-07-29 20:55:06 +02:00
Gert Wohlgemuth
5871488858 fixed errors and making flake pass 2018-07-29 20:55:06 +02:00
xmatthias
2e6e5029ba fix mypy and tests 2018-07-29 20:55:06 +02:00
Gert Wohlgemuth
19b9966417 satisfied flake8 again 2018-07-29 20:55:06 +02:00
Gert Wohlgemuth
57f683697d revised code 2018-07-29 20:55:06 +02:00
Gert Wohlgemuth
296d3d8bbe working on refacturing of the strategy class 2018-07-29 20:55:06 +02:00
Matthias
336cd524a3 Merge pull request #1094 from freqtrade/pyup-scheduled-update-2018-07-29
Scheduled daily dependency update on sunday
2018-07-29 19:02:17 +02:00
Janne Sinivirta
f832edf5bc remove useless docstrings from tests 2018-07-29 17:09:44 +03:00
Janne Sinivirta
1bbb86c621 remove nonsense asserts 2018-07-29 16:23:17 +03:00
pyup-bot
2ef35400c9 Update pytest from 3.6.3 to 3.6.4 2018-07-29 14:24:08 +02:00
pyup-bot
9c7f53d90d Update ccxt from 1.17.39 to 1.17.45 2018-07-29 14:24:06 +02:00
Matthias
ebfcc0fc13 install numpy before ta-lib to fix build errors 2018-07-29 14:01:50 +02:00
Matthias
42024134ec Merge pull request #1092 from freqtrade/revert-1090-ujson-loader
Revert "backtesting: try to load data with ujson if it exists"
2018-07-29 12:23:25 +01:00
Matthias
7f27beff4b Revert "backtesting: try to load data with ujson if it exists" 2018-07-29 13:23:11 +02:00
creslinux
dd71071740 Added logger.info when Sandbox is enabled. 2018-07-29 09:15:13 +00:00
creslinux
c85c7a3a77 Documentation fixes. 2018-07-29 09:12:05 +00:00
creslinux
1e804c0df5 flake 8 2018-07-29 08:10:55 +00:00
creslinux
fc06d028b8 Unit tests for sandbox pass / fail scenarios
Big Wave of appreciation to xmatthias for the guidence on how
Mocker works
2018-07-29 08:02:04 +00:00
Matthias
618784d060 Merge pull request #1090 from freqtrade/ujson-loader
backtesting: try to load data with ujson if it exists
2018-07-29 08:54:02 +01:00
Samuel Husso
cfcc2e61e5 Merge pull request #1088 from freqtrade/fix/unpatched_mock
fix rpc test going to network
2018-07-29 09:53:52 +03:00
Samuel Husso
187e039a58 Merge pull request #1034 from freqtrade/feat/positive_sl_limit
add offset for positive trailing stop loss
2018-07-29 08:30:29 +03:00
Gert
b3df1b1ba7 added documentation: 2018-07-28 21:31:20 -07:00
creslinux
0a059662b3 Submitting with unit test for the working scenario.
Strongly recommend core team check the unit test is even targetting the
correct code in exchange/__init__.py

I have a real knowledge gap on mocker, in so far as how tests map to
what they're targeting.
2018-07-28 20:32:10 +00:00
Samuel Husso
cb2fff8909 mypy doesn't handle common idiomacy so disable the line (see the open issue more details) 2018-07-28 22:06:26 +03:00
Samuel Husso
cdd8cc551c backtesting: try to load data with ujson if it exists 2018-07-28 21:56:11 +03:00
creslinux
8648ac9da2 Update documentation with hot to sandbox test.
Allowing end-to-end GDAX API use without risking real money.
2018-07-28 17:42:56 +00:00
Samuel Husso
083befaafc Merge pull request #1087 from freqtrade/pyup-scheduled-update-2018-07-28
Scheduled daily dependency update on saturday
2018-07-28 16:26:38 +03:00
pyup-bot
099e7020c8 Update ccxt from 1.17.29 to 1.17.39 2018-07-28 14:24:06 +02:00
Samuel Husso
6ab8fa8c71 Merge pull request #1079 from creslinux/apiAuthPass
add Password option to API login, GDAX as example requires.
2018-07-28 13:53:39 +03:00
creslinux
b2b81c8b2d Update documentation with hot to sandbox test.
Allowing end-to-end GDAX API use without risking real money.
2018-07-27 20:18:12 +00:00
Matthias
243b63e39c fix rpc test going to network (unsuitable for flights...) 2018-07-27 21:14:41 +01:00
Janne Sinivirta
a3d870ad3e Merge pull request #1075 from freqtrade/extract_get_history
Extract get history from get_signal call
2018-07-27 20:54:20 +03:00
Matthias
1ceaa2200a Merge pull request #1080 from freqtrade/pyup-scheduled-update-2018-07-27
Scheduled daily dependency update on friday
2018-07-27 16:06:07 +01:00
Matthias
c8ac98501c Merge pull request #1081 from sandoche/patch-1
Error fixed in the quickstart documentation
2018-07-27 16:05:51 +01:00
Sandoche ADITTANE
ca0d658f15 Error fixed in the quickstart documentation 2018-07-27 15:28:06 +02:00
pyup-bot
4547ae930a Update ccxt from 1.17.20 to 1.17.29 2018-07-27 14:24:06 +02:00
creslin
40ae250193 Update constants.py
Adding UID also, as itll get ran into in future on an exchange that needs it.
2018-07-27 12:19:01 +00:00
creslinux
c47253133a have to begin before we can stop 2018-07-27 12:07:07 +00:00
creslinux
7efa81073a Removed ; at line end. 2018-07-27 09:10:09 +00:00
creslinux
d23b3ccc5e odd cut and paste error fixed. 2018-07-27 08:55:36 +00:00
Matthias
48cd468b6c Don't do all network calls at once without async 2018-07-27 07:40:27 +01:00
Matthias
df3e76a65d Remove legacy code, fix missed call 2018-07-26 19:11:51 +01:00
Matthias
f2a9be3684 Adjust tests and remove legacy variable 2018-07-26 19:06:25 +01:00
Matthias
3324cdfcbe add mock for get_history in patch_get_signal 2018-07-26 18:58:49 +01:00
Matthias
484103b957 extract get_history_data from get_signal 2018-07-26 18:23:42 +01:00
Samuel Husso
6e437a7290 Merge pull request #1074 from freqtrade/pyup-scheduled-update-2018-07-26
Scheduled daily dependency update on thursday
2018-07-26 15:48:41 +03:00
pyup-bot
0c7ceadb27 Update ccxt from 1.17.11 to 1.17.20 2018-07-26 14:24:05 +02:00
Janne Sinivirta
726b94b077 Merge pull request #1069 from freqtrade/feat/movefiatconverttorpc
Feat/movefiatconverttorpc
2018-07-26 14:25:58 +03:00
Matthias
452a1cad9d don't default fiat_convert to None for outputs 2018-07-26 07:26:23 +01:00
Matthias
7b49f746d1 remove #FIX which was fixed 2018-07-25 22:47:20 +01:00
Matthias
78f8c6566e Merge pull request #1072 from freqtrade/datesorting-backtest-fix
Use pandas own min and max
2018-07-25 22:45:24 +01:00
Janne Sinivirta
4b38c8b11d use pandas own min and max for column sorting 2018-07-25 17:04:25 +03:00
Samuel Husso
3fa1c5b19f Merge pull request #1070 from freqtrade/pyup-scheduled-update-2018-07-25
Scheduled daily dependency update on wednesday
2018-07-25 07:33:00 -05:00
pyup-bot
4f4daf4071 Update ccxt from 1.16.89 to 1.17.11 2018-07-25 14:24:07 +02:00
Matthias
dc1ad3cbf6 whitespace issues 2018-07-24 23:08:40 +01:00
Matthias
ff6435948e Fix random test failure 2018-07-24 22:53:10 +01:00
Matthias
23c2a75fc4 Merge pull request #1066 from freqtrade/pyup-scheduled-update-2018-07-24
Scheduled daily dependency update on tuesday
2018-07-24 13:53:35 +01:00
pyup-bot
7feea8c7a6 Update numpy from 1.14.5 to 1.15.0 2018-07-24 14:24:08 +02:00
pyup-bot
cf6e229729 Update ccxt from 1.16.88 to 1.16.89 2018-07-24 14:24:06 +02:00
Matthias
4928686af9 Remove currency from daily table 2018-07-24 09:37:25 +01:00
Matthias
30b72ad98a don't show fiat-currency if not set 2018-07-24 08:20:32 +01:00
Matthias
1a9ead45eb fix missed fiat_display_currency config value 2018-07-24 08:00:56 +01:00
Janne Sinivirta
0b3190552e Merge pull request #1018 from freqtrade/feat/sell_reason
Record sell reason
2018-07-24 09:09:45 +03:00
Matthias
456e49fe35 default fiat_currency to none 2018-07-24 00:01:51 +01:00
Janne Sinivirta
ab67822af2 Merge pull request #1062 from freqtrade/fix/migratescript
fix a bug in the database migration script
2018-07-23 16:48:12 +03:00
Janne Sinivirta
7f877aed6f Merge pull request #1063 from freqtrade/pyup-scheduled-update-2018-07-23
Scheduled daily dependency update on monday
2018-07-23 16:47:19 +03:00
pyup-bot
4575919d78 Update ccxt from 1.16.86 to 1.16.88 2018-07-23 14:24:05 +02:00
Matthias
10fc2c67c7 Fix bug causing a database-migration to fail from aspecific state 2018-07-23 09:10:37 +01:00
Matthias
643de58c4d Add test to check for a mid-migrated database (not old but not new) 2018-07-23 09:09:56 +01:00
Janne Sinivirta
aba3c69765 Merge pull request #1061 from freqtrade/fix_networkcall
Add missing mock
2018-07-23 07:19:37 +03:00
Matthias
0775a371fe rename sellreason to sell_Reason, fix typos 2018-07-23 00:54:20 +01:00
Matthias
23fe0db2df Add missing mock 2018-07-22 17:06:42 +01:00
Matthias
f54ac5a8de revert bugfix done in it's own branch 2018-07-22 17:05:22 +01:00
Matthias
4c8411537f Don't require fiat-currency 2018-07-22 14:53:46 +02:00
Matthias
bd2771b8f9 use correct property 2018-07-22 14:52:58 +02:00
Matthias
4d864df59e Add tests for no_fiat functionality 2018-07-22 14:49:07 +02:00
Matthias
fae4c3a4e3 only init if stake_currency is set 2018-07-22 14:48:06 +02:00
Matthias
2b297869a1 adjust checks to fit new functionality 2018-07-22 14:35:59 +02:00
Matthias
6cc0a72bca ADd optional to class _fiat_convert 2018-07-22 14:35:37 +02:00
Samuel Husso
f53e03767c Merge pull request #1060 from freqtrade/pyup-scheduled-update-2018-07-22
Scheduled daily dependency update on sunday
2018-07-22 07:34:40 -05:00
pyup-bot
5ab1e66978 Update ccxt from 1.16.80 to 1.16.86 2018-07-22 14:24:05 +02:00
Samuel Husso
849ded7772 Merge pull request #1057 from freqtrade/fix/fiatconvert_error
Catch all exceptions from fiat-convert api calls
2018-07-21 23:12:56 -05:00
Matthias
f297d22edb fix some tests in rpc_telegram 2018-07-21 20:49:57 +02:00
Matthias
0681a806cc move cryptofiatconvert to rpc 2018-07-21 20:44:38 +02:00
Matthias
be3f04775a remove unnecessary mocks - add mocks which went to exchange 2018-07-21 20:21:00 +02:00
Matthias
9467461160 only init FIATConvert when telegram is enabled 2018-07-21 20:13:32 +02:00
Matthias
66af41192a Catch all exceptions from fiat-convert api calls 2018-07-21 19:50:38 +02:00
Matthias
6f7898809a Merge pull request #1055 from freqtrade/pyup-scheduled-update-2018-07-21
Scheduled daily dependency update on saturday
2018-07-21 14:40:26 +02:00
pyup-bot
ab3478a742 Update ccxt from 1.16.75 to 1.16.80 2018-07-21 14:24:05 +02:00
Matthias
00fa41d63f Merge pull request #1051 from freqtrade/pyup-scheduled-update-2018-07-20
Scheduled daily dependency update on friday
2018-07-20 15:52:32 +02:00
pyup-bot
7f6c79eb76 Update ccxt from 1.16.68 to 1.16.75 2018-07-20 14:24:06 +02:00
Janne Sinivirta
b45128f53d Merge pull request #1050 from freqtrade/xmatt_verbosity2
Add multiple verbosity levels
2018-07-20 11:42:42 +03:00
Matthias
dd1290e38e Add multiple verbosity levels 2018-07-19 21:12:27 +02:00
Janne Sinivirta
62701888c9 Merge pull request #1049 from freqtrade/revert-1045-xmatt_verbosity
Revert "Add more verbosity levels"
2018-07-19 21:50:46 +03:00
Matthias
90915b6b2f Revert "Add more verbosity levels" 2018-07-19 20:43:41 +02:00
Matthias
1b2bfad348 Fix wrong test 2018-07-19 20:36:49 +02:00
Matthias
060469fefc Add stuff after rebase 2018-07-19 20:12:20 +02:00
Matthias
4fb9823cfb fix rebase problem 2018-07-19 19:50:06 +02:00
Matthias
760c79c5e9 Use .center() to output trades header line 2018-07-19 19:39:08 +02:00
Matthias
a452864b41 Use namedtuple for sell_return 2018-07-19 19:39:08 +02:00
Matthias
ad98c62329 update backtest anlaysis cheatsheet 2018-07-19 19:34:14 +02:00
Matthias
506aa0e3d3 Add print_sales table and test 2018-07-19 19:34:14 +02:00
Matthias
426c25f631 record ticker_interval and strategyname 2018-07-19 19:34:14 +02:00
Matthias
4059871c28 Add get_strategy_name 2018-07-19 19:34:14 +02:00
Matthias
2a61629014 Export sell_reason from backtest 2018-07-19 19:29:31 +02:00
Matthias
8c0b19f80c Check sell-reason for sell-reason-specific tests 2018-07-19 19:29:31 +02:00
Matthias
838b0e7b76 Remove unused import 2018-07-19 19:29:31 +02:00
Matthias
cbffd3650b add sell_reason to backtesting 2018-07-19 19:29:31 +02:00
Matthias
0147b1631a remove optional from selltype 2018-07-19 19:27:33 +02:00
Matthias
49a7c7f08e fix tests 2018-07-19 19:27:33 +02:00
Janne Sinivirta
1af24af391 Merge pull request #1047 from freqtrade/pyup-scheduled-update-2018-07-19
Scheduled daily dependency update on thursday
2018-07-19 17:34:02 +03:00
Janne Sinivirta
0cc1b66ae7 Merge pull request #1037 from freqtrade/fix/backtest-comment
replace --realistic with 2 separate flags
2018-07-19 17:33:19 +03:00
Janne Sinivirta
6070d819b8 Merge pull request #1040 from freqtrade/xmatthias_backtest_duration
Fix backtest duration calculation
2018-07-19 17:32:11 +03:00
pyup-bot
f2bfc9ccc2 Update ccxt from 1.16.57 to 1.16.68 2018-07-19 14:24:07 +02:00
Matthias
f991109b0a Add sell-reason to sell-tree 2018-07-19 13:29:42 +02:00
Matthias
6bb7167b56 Add sellType enum 2018-07-19 13:25:48 +02:00
Matthias
365ba98131 add option to full_json example 2018-07-19 13:22:44 +02:00
Matthias
6a3c8e3933 update docs for trailing stoploss offset 2018-07-19 13:22:44 +02:00
Matthias
c0a7725c1f Add stoploss offset 2018-07-19 13:22:44 +02:00
Matthias
71100a67c9 update documentation with new options 2018-07-19 13:20:15 +02:00
Matthias
8f254031c6 Add short form for parameters, change default for hyperopt 2018-07-19 13:19:36 +02:00
Matthias
aa69177436 Properly check emptyness and adjust floatfmt 2018-07-19 13:14:21 +02:00
Matthias
64f933477d Merge pull request #1007 from freqtrade/remove-analyze
Remove Analyze
2018-07-19 10:12:36 +02:00
Janne Sinivirta
aaa58a956d Merge pull request #1045 from freqtrade/xmatt_verbosity
Add more verbosity levels
2018-07-19 08:11:32 +03:00
Matthias
75c0a476f8 Test setting verbosity in commandline 2018-07-18 23:40:04 +02:00
Matthias
1ab7f5fb6d add tests for more debug levels 2018-07-18 22:53:44 +02:00
Matthias
789b98015f Allow different loglevels 2018-07-18 22:52:57 +02:00
Matthias
7134c15e86 Merge pull request #1024 from freqtrade/feature/webhook
Feature/webhook
2018-07-18 20:39:57 +02:00
Matthias
79b1030435 output duration in a more readable way 2018-07-18 20:08:55 +02:00
Matthias
ac6955fd3b Merge pull request #1041 from freqtrade/pyup-scheduled-update-2018-07-18
Scheduled daily dependency update on wednesday
2018-07-18 14:39:57 +02:00
pyup-bot
a374f95687 Update ccxt from 1.16.50 to 1.16.57 2018-07-18 14:24:07 +02:00
Matthias
f9f6a3bd04 cast to int to keep exports constant 2018-07-18 09:29:51 +02:00
Matthias
8e4d2abd4e Fix typo 2018-07-18 09:10:17 +02:00
Matthias
08237abe20 Fix wrong backtest duration
identified in #1038
2018-07-18 09:06:12 +02:00
Matthias
5b3fa3c635 Merge pull request #1039 from Lufedi/develop
Add docs to get_trade_stake_amount function
2018-07-18 08:57:56 +02:00
Luis Felipe Diaz Chica
ee8e890f50 Add docs to get_trade_stake_amount function 2018-07-18 01:36:39 -05:00
Matthias
3df79b8542 fix hanging intend 2018-07-17 21:12:05 +02:00
Matthias
a290286fef update documentation 2018-07-17 21:05:31 +02:00
Matthias
c82276ecbe add --disable-max-market-positions 2018-07-17 21:05:03 +02:00
Matthias
b29eed32ca update documentation 2018-07-17 20:29:53 +02:00
Matthias
e17618407b Rename --realistic-simulation to --enable-position-stacking 2018-07-17 20:26:59 +02:00
Janne Sinivirta
85fd4dd3ff rename analyze.py to exchange_helpers.py 2018-07-17 21:26:52 +03:00
Matthias
78205da4f0 Merge pull request #1036 from freqtrade/pyup-scheduled-update-2018-07-17
Scheduled daily dependency update on tuesday
2018-07-17 14:40:25 +02:00
pyup-bot
e021d22c7f Update ccxt from 1.16.36 to 1.16.50 2018-07-17 14:24:09 +02:00
Janne Sinivirta
4a26eb34ea fix plot_profit to use strategy instead of Analyze 2018-07-17 11:47:09 +03:00
Janne Sinivirta
50b15b8052 fix plot_dataframe to use strategy instead of Analyze 2018-07-17 11:41:21 +03:00
Janne Sinivirta
e11ec28962 remove leftover commented-out code 2018-07-17 11:13:35 +03:00
Janne Sinivirta
06d024cc46 make pytest ignore this file 2018-07-17 11:07:27 +03:00
Janne Sinivirta
084264669f fix the last failing unit test 2018-07-17 11:02:07 +03:00
Janne Sinivirta
dbc3874b4f __init__ must return None to please mypy 2018-07-17 10:47:15 +03:00
Janne Sinivirta
78af4bc785 move and fix tests from Analyze to interface of strategy 2018-07-17 10:23:04 +03:00
Matthias
2795db3ea0 Merge pull request #1033 from freqtrade/pyup-scheduled-update-2018-07-16
Scheduled daily dependency update on monday
2018-07-16 15:02:44 +02:00
pyup-bot
4f957728bf Update scikit-learn from 0.19.1 to 0.19.2 2018-07-16 14:24:07 +02:00
pyup-bot
62f4d734b9 Update ccxt from 1.16.33 to 1.16.36 2018-07-16 14:24:06 +02:00
Samuel Husso
a3466f4b42 Merge pull request #1031 from freqtrade/feat/update_configdict
Update config dict with attributes loaded from strategy
2018-07-16 10:00:46 +03:00
Samuel Husso
050afe2bc0 Merge pull request #979 from creslinux/Check_timeframes
Handle if ticker_interval in config.json is not supported on exchange.
2018-07-16 09:57:46 +03:00
Janne Sinivirta
5c87c420c7 restore one analyze test 2018-07-16 08:59:14 +03:00
Janne Sinivirta
aeb4102bcb refactor Analyze class methods to base Strategy class 2018-07-16 08:23:39 +03:00
Janne Sinivirta
f6b8c2b40f move parse_ticker_dataframe outside Analyze class 2018-07-16 08:23:39 +03:00
Janne Sinivirta
85e6c9585a remove pass-through methods from Analyze 2018-07-16 08:23:39 +03:00
Janne Sinivirta
a74147c472 move strategy initialization outside Analyze 2018-07-16 08:23:39 +03:00
Matthias
727f569e3a Merge pull request #1032 from freqtrade/pyup-scheduled-update-2018-07-15
Scheduled daily dependency update on sunday
2018-07-15 14:42:35 +02:00
pyup-bot
8f59759e97 Update ccxt from 1.16.16 to 1.16.33 2018-07-15 14:24:05 +02:00
Matthias
158226012a consistent use of the config dict within the test 2018-07-15 09:08:14 +02:00
Matthias
b4ba641131 Update config dict with attributes loaded from strategy 2018-07-15 09:01:08 +02:00
Matthias
682f4c1ade Merge pull request #1030 from freqtrade/pyup-scheduled-update-2018-07-14
Scheduled daily dependency update on saturday
2018-07-14 19:39:13 +02:00
pyup-bot
e1de988f85 Update sqlalchemy from 1.2.9 to 1.2.10 2018-07-14 14:24:09 +02:00
pyup-bot
bc83c34118 Update ccxt from 1.16.12 to 1.16.16 2018-07-14 14:24:07 +02:00
Matthias
278e7159bc adjust webhook tests 2018-07-14 13:32:35 +02:00
Matthias
1284627219 move url to private class level 2018-07-14 13:32:35 +02:00
Matthias
120fc29643 use dict comprehension 2018-07-14 13:32:35 +02:00
Matthias
6336d8a0e2 remove copy leftover 2018-07-14 13:32:35 +02:00
Matthias
ee2f6ccbe9 Add test for enable_webhook 2018-07-14 13:32:35 +02:00
Matthias
144d308e5e Allow enabling of webhook 2018-07-14 13:32:35 +02:00
Matthias
3ca161f196 Add webhook config 2018-07-14 13:32:35 +02:00
Matthias
f55df7ba63 improve README.md formatting (styling only) 2018-07-14 13:32:35 +02:00
Matthias
71df41c4eb add documentation for rpc_webhook 2018-07-14 13:32:35 +02:00
Matthias
a4643066a8 allow more flexibility in webhook 2018-07-14 13:32:35 +02:00
Matthias
25250f7c10 don't hardcode post parameters 2018-07-14 13:32:35 +02:00
Matthias
fa8512789f add tests for webhook 2018-07-14 13:32:35 +02:00
Matthias
ae22af1ea3 fix typo 2018-07-14 13:32:35 +02:00
Matthias
6e16c1d80d add webhook test file 2018-07-14 13:32:35 +02:00
Matthias
266092a05d Merge pull request #1029 from freqtrade/mypy-fix
rpc: dont re-use variables with different types
2018-07-14 13:15:39 +02:00
Samuel Husso
fa8b349200 rpc: dont re-use variables with different types 2018-07-14 08:02:39 +03:00
Samuel Husso
04bed3e53e Merge pull request #1027 from peterkorodi/patch-2
Update plotting.md
2018-07-13 22:50:10 -05:00
peterkorodi
68ddd1b951 Update plotting.md
Fix pairs and db-url in the doc
2018-07-14 00:07:38 +02:00
Samuel Husso
b6e1020f39 Merge pull request #1026 from freqtrade/pyup-scheduled-update-2018-07-13
Scheduled daily dependency update on friday
2018-07-13 08:56:51 -05:00
pyup-bot
5b02b87735 Update ccxt from 1.16.6 to 1.16.12 2018-07-13 14:24:06 +02:00
Matthias
c17e8d6abb Merge pull request #972 from freqtrade/feature/rewrite-rpc
Rewrite RPC module
2018-07-12 19:38:01 +02:00
gcarq
cb8cd21e22 add tests for telegram.send_msg 2018-07-12 17:50:11 +02:00
gcarq
a559e22f16 remove duplicate send_msg invocation 2018-07-12 17:29:02 +02:00
gcarq
7eaeb8d146 status: return arrow object instead humanized str 2018-07-12 17:27:40 +02:00
gcarq
0920fb6120 use more granular msg dict for buy/sell notifications 2018-07-12 17:16:31 +02:00
gcarq
4cb1aa1d97 use dict as argument for rpc.send_msg 2018-07-12 17:12:42 +02:00
gcarq
96a405feb7 implement name property in abstract class 2018-07-12 17:11:31 +02:00
gcarq
112998c205 refactor _rpc_balance 2018-07-12 17:11:31 +02:00
gcarq
f1a370b3b9 return dict from _rpc_status and handle rendering in module impl 2018-07-12 17:10:04 +02:00
gcarq
29670b9814 remove markdown formatting from exception string 2018-07-12 17:07:19 +02:00
gcarq
df8ba28ce5 convert start, stop and reload_conf to return a dict 2018-07-12 17:07:19 +02:00
Matthias
5288e18f2f Merge pull request #1022 from freqtrade/pyup-scheduled-update-2018-07-12
Scheduled daily dependency update on thursday
2018-07-12 14:33:14 +02:00
pyup-bot
ddfc4722b9 Update ccxt from 1.15.42 to 1.16.6 2018-07-12 14:23:06 +02:00
Janne Sinivirta
bd46b4faf3 Merge pull request #1015 from freqtrade/xmatthias-patch-1
add missing s to Backtest cum results
2018-07-11 16:18:07 +03:00
Matthias
46708e7d29 Merge pull request #1014 from freqtrade/pyup-scheduled-update-2018-07-11
Scheduled daily dependency update on wednesday
2018-07-11 14:50:09 +02:00
Matthias
06c9494a46 add missing s to Backtest cum results 2018-07-11 14:50:04 +02:00
pyup-bot
8f6252b312 Update ccxt from 1.15.35 to 1.15.42 2018-07-11 14:23:06 +02:00
Janne Sinivirta
1f16ff268f Merge pull request #1010 from jblestang/refactoring_create_trade_function
Refactoring Create Trade
2018-07-11 07:23:03 +03:00
Janne Sinivirta
aa2366346a Merge pull request #1001 from xmatthias/feat/backtest_cum_profit
Add cumulative profit to backtest result table
2018-07-11 07:21:28 +03:00
Janne Sinivirta
8b72560eba Merge pull request #1006 from freqtrade/update_plotly
Update plotly
2018-07-11 07:20:33 +03:00
Jean-Baptiste LE STANG
773fb5953b Reafcotring Create Trade 2018-07-10 15:10:56 +02:00
Matthias
3540ba3712 Merge pull request #1009 from freqtrade/pyup-scheduled-update-2018-07-10
Scheduled daily dependency update on tuesday
2018-07-10 14:35:33 +02:00
pyup-bot
d546a4b29f Update ccxt from 1.15.28 to 1.15.35 2018-07-10 14:23:08 +02:00
Janne Sinivirta
b4be3c2499 Merge pull request #1002 from xmatthias/test/use_open_backtest
Use open-rates for backtesting
2018-07-10 09:20:32 +03:00
Matthias
85c60519b0 Fix test crash 2018-07-09 22:11:12 +02:00
Matthias
6be6448334 replace "transparent" with rgb to fix exception in plotly 3.0.0 2018-07-09 21:56:29 +02:00
Matthias
f5bc65b877 update plotly 2018-07-09 21:56:24 +02:00
Matthias
a7a82635b4 Merge pull request #1004 from berlinguyinca/patch-2
Fixing database issues
2018-07-09 21:54:21 +02:00
Samuel Husso
b9916b60f9 Merge pull request #1005 from freqtrade/pyup-scheduled-update-2018-07-09
Scheduled daily dependency update on monday
2018-07-09 08:26:54 -05:00
pyup-bot
b773e3472a Update ccxt from 1.15.27 to 1.15.28 2018-07-09 14:23:06 +02:00
Gert Wohlgemuth
4654792784 Fixing database issues
1. if database is defined in config file, it currently tosses an exception that only export file or db is defined
2. if trades are loaded from databases, plot crashes with an exception 'cannot compare tz-naive and tz-aware datetime-like objects'
3. if Trade is not closed, crashes with exception that NoneType has no field timestamp

all should be fixed
2018-07-08 22:43:34 -07:00
Matthias
750d737b7d Add tests for change to open_rate 2018-07-08 20:18:34 +02:00
Matthias
0bd9674b5c Merge pull request #1000 from pan-long/fix-doc
Update doc for manually fix trade
2018-07-08 20:07:25 +02:00
Matthias
8b06000f0f Use open-rates for backtesting 2018-07-08 20:03:11 +02:00
Matthias
efaa8f16e7 Improve formattiong of table 2018-07-08 20:01:33 +02:00
Matthias
38487644f0 fix tests for backtest-result output table 2018-07-08 19:55:16 +02:00
Matthias
1a24afef77 add cumsum to backtest-results 2018-07-08 19:55:04 +02:00
Janne Sinivirta
8fb146ba6a Merge pull request #992 from freqtrade/backtest_optimize
reduce calculation effort by removing a call to calc_profit_percent
2018-07-08 17:41:50 +03:00
Janne Sinivirta
05b078b8dd Merge pull request #999 from freqtrade/pyup-scheduled-update-2018-07-08
Scheduled daily dependency update on sunday
2018-07-08 17:40:42 +03:00
Janne Sinivirta
6926e468a4 Merge pull request #984 from freqtrade/test_backtest_results
Test backtest results
2018-07-08 17:40:12 +03:00
Janne Sinivirta
34764108cc Merge pull request #997 from freqtrade/fix/timedout_candle
don't flag data as outdated which isn't
2018-07-08 17:36:03 +03:00
pyup-bot
17c9c183f5 Update pandas from 0.23.2 to 0.23.3 2018-07-08 14:23:07 +02:00
pyup-bot
cc107bb3cc Update ccxt from 1.15.25 to 1.15.27 2018-07-08 14:23:05 +02:00
Matthias
8dd6e29426 don't flag data as outdated which isn't 2018-07-08 13:34:47 +02:00
Matthias
3e03a208f1 reduce calculation effort (slightly!) 2018-07-07 20:17:53 +02:00
Matthias
570d27a0c4 Add testcase where ticker_interval is not in the configuration 2018-07-07 15:30:29 +02:00
Samuel Husso
7c8c8e83d3 Merge pull request #990 from freqtrade/update_dockerfile
Update Dockerfile to 3.6.6
2018-07-07 08:15:20 -05:00
Matthias
2b488d1da2 Update Dockerfile to 3.6.6 2018-07-07 14:52:39 +02:00
Matthias
e98efe3a35 Merge pull request #989 from freqtrade/pyup-scheduled-update-2018-07-07
Scheduled daily dependency update on saturday
2018-07-07 14:43:32 +02:00
Matthias
3f6e9cd28f Add tests for validate_timeframes 2018-07-07 14:42:53 +02:00
Matthias
af17cef002 fix existing tests to work with validate_timeframes 2018-07-07 14:41:42 +02:00
pyup-bot
742fefa786 Update pandas from 0.23.1 to 0.23.2 2018-07-07 14:23:08 +02:00
pyup-bot
08fe10e302 Update ccxt from 1.15.21 to 1.15.25 2018-07-07 14:23:06 +02:00
Matthias
9906da46f6 move comment to correct place 2018-07-06 20:00:54 +02:00
Matthias
54976fa103 Add more tests to validate buy/sell rows 2018-07-06 19:56:16 +02:00
Samuel Husso
e1d7c72bb8 Merge pull request #983 from freqtrade/pyup-scheduled-update-2018-07-06
Scheduled daily dependency update on friday
2018-07-06 09:41:10 -05:00
pyup-bot
af03c17209 Update ccxt from 1.15.13 to 1.15.21 2018-07-06 14:23:06 +02:00
Gert Wohlgemuth
1897a1cb6a fixed mypy issues, seriosuly... 2018-07-05 16:10:38 -07:00
Gert Wohlgemuth
58879ff012 fixed braket 2018-07-05 15:01:53 -07:00
Gert Wohlgemuth
e1f5745f59 Update resolver.py 2018-07-05 14:50:23 -07:00
Gert Wohlgemuth
1c48902e64 Merge branch 'develop' into BASE64 2018-07-05 14:40:04 -07:00
Gert Wohlgemuth
8bbee4038b integrated BASE64 encoded strategy loading 2018-07-05 14:30:24 -07:00
Matthias
c35d1b9c9d Add test which checks the backtest result 2018-07-05 23:22:35 +02:00
Matthias
4f642b769c Merge pull request #981 from freqtrade/fstrings-in-use
Fstrings in use
2018-07-05 22:18:15 +02:00
Samuel Husso
e808b3a2a1 rpc: get rid of extra else and fix mypy warning 2018-07-05 10:47:08 -05:00
Samuel Husso
df68b0990f rpc: fstrings 2018-07-05 10:11:29 -05:00
Samuel Husso
adbffc69e1 telegram: fstrings in use 2018-07-05 10:11:29 -05:00
Samuel Husso
21fc933678 convert_backtesting: fstrings in use 2018-07-05 10:11:29 -05:00
Samuel Husso
a2063ede55 persistence: fstrings in use 2018-07-05 10:11:29 -05:00
Samuel Husso
7dca3c6d03 freqtradebot,main,hyperopt: fstrings in use 2018-07-05 10:11:29 -05:00
Samuel Husso
03c112a601 config, optimize: fstrings in use 2018-07-05 10:11:29 -05:00
Matthias
c77686c7a7 Merge pull request #980 from freqtrade/pyup-scheduled-update-2018-07-05
Scheduled daily dependency update on thursday
2018-07-05 15:39:57 +02:00
pyup-bot
239f8606e1 Update pytest from 3.6.2 to 3.6.3 2018-07-05 14:23:12 +02:00
pyup-bot
bfd1e90154 Update ccxt from 1.15.8 to 1.15.13 2018-07-05 14:23:11 +02:00
creslinux
5ab644dea6 flake 8 fix 2018-07-05 12:05:31 +00:00
creslinux
966668f48a Handle if ticker_interval in config.json is not supported on exchange.
Returns.

Tested positive and negative data.
The ticker list in constants.py may be obsolete now, im not sure.

 raise OperationalException(f'Invalid ticker {timeframe}, this Exchange supports {timeframes}')
freqtrade.OperationalException: Invalid ticker 14m, this Exchange supports {'1m': '1m', '3m': '3m', '5m': '5m', '15m': '15m', '30m': '30m', '1h': '1h', '2h': '2h', '4h': '4h', '6h': '6h', '8h': '8h', '12h': '12h', '1d': '1d', '3d': '3d', '1w': '1w', '1M': '1M'}
2018-07-05 11:57:59 +00:00
Samuel Husso
d8d0579c5a Merge pull request #930 from freqtrade/skopt
Replace Hyperopt with scikit-optimize
2018-07-04 13:51:14 -05:00
Michael Egger
64c68d93c3 Merge pull request #976 from freqtrade/sort-imports
sort imports
2018-07-04 16:59:42 +02:00
Matthias
700f02dde8 Merge pull request #977 from freqtrade/pyup-scheduled-update-2018-07-04
Scheduled daily dependency update on wednesday
2018-07-04 15:26:32 +02:00
pyup-bot
ac20bf31df Update ccxt from 1.15.7 to 1.15.8 2018-07-04 14:23:06 +02:00
Janne Sinivirta
bf4d0a9b70 sort imports 2018-07-04 10:31:35 +03:00
Janne Sinivirta
96bb2efe69 use joblib.dump and load for trials 2018-07-03 23:08:29 +03:00
Janne Sinivirta
c4a8435e00 change pickle file name to better suit it's current purpose 2018-07-03 22:17:43 +03:00
Janne Sinivirta
9dbe0f50a3 fix tests after changing the dumping and pickling dataframe in hyperopt 2018-07-03 22:09:59 +03:00
Janne Sinivirta
3a7056ea1b run at least one epoch 2018-07-03 21:55:22 +03:00
Janne Sinivirta
2cde540645 remove dead code 2018-07-03 21:50:45 +03:00
Janne Sinivirta
ef59f9ad24 sort imports in hyperopt.py 2018-07-03 21:50:24 +03:00
Matthias
e91cfbfeeb Merge pull request #975 from freqtrade/pyup-scheduled-update-2018-07-03
Scheduled daily dependency update on tuesday
2018-07-03 14:35:45 +02:00
pyup-bot
2c0e950486 Update ccxt from 1.15.3 to 1.15.7 2018-07-03 14:23:05 +02:00
Janne Sinivirta
ee4754cfb9 avoid re-serialization of whole dataframe 2018-07-03 14:49:58 +03:00
Janne Sinivirta
4a26b88a17 improve documentation 2018-07-03 12:51:02 +03:00
Janne Sinivirta
2713fdb860 use cpu count explicitly in job count 2018-07-03 11:46:56 +03:00
Janne Sinivirta
79aab4cce2 use fstring 2018-07-03 11:44:54 +03:00
Samuel Husso
2b34d10973 Merge pull request #973 from freqtrade/pyup-scheduled-update-2018-07-02
Scheduled daily dependency update on monday
2018-07-02 08:57:27 -05:00
pyup-bot
76343ecb77 Update ccxt from 1.14.301 to 1.15.3 2018-07-02 14:23:06 +02:00
Janne Sinivirta
fa8fc3e4ce handle the case where we have zero buys 2018-07-02 11:46:55 +03:00
Janne Sinivirta
aec3f582e1 Merge branch 'develop' into skopt 2018-07-02 11:27:27 +03:00
Janne Sinivirta
a58d51ded0 update hyperopt documentation 2018-07-02 09:56:58 +03:00
Michael Egger
5e4a6ba7ba Merge pull request #963 from freqtrade/feat/stop_loss
Feat/stop loss
2018-07-01 20:50:13 +02:00
xmatthias
3c5be55eb9 remove unnecessary variable 2018-07-01 20:17:30 +02:00
xmatthias
782570e71e Address PR comment 2018-07-01 20:03:07 +02:00
Matthias
ed2a1becef Merge branch 'develop' into feat/stop_loss 2018-07-01 20:01:02 +02:00
xmatthias
937644a04b change while-loop to enumerate - add intensified test for this scenario 2018-07-01 19:55:51 +02:00
xmatthias
e39d88ef65 Address some PR comments 2018-07-01 19:54:26 +02:00
Michael Egger
f91263c8ef Merge pull request #966 from freqtrade/feat/revamp_exchangetest
Rewrite standard ccxt exception handling
2018-07-01 19:47:57 +02:00
Michael Egger
e2127f5af1 Merge pull request #969 from xmatthias/split_unfilled
separating unfulfilled timeouts for buy and sell
2018-07-01 19:47:24 +02:00
xmatthias
2dc881558d address PR comments 2018-07-01 19:41:19 +02:00
xmatthias
c66f858b98 rename innerfun to mock_ccxt_fun 2018-07-01 19:37:55 +02:00
Michael Egger
8023fdf923 Merge pull request #971 from freqtrade/fix/nonmocked_markets
Add get_markets mock to new tests
2018-07-01 15:11:22 +02:00
Michael Egger
2cee8e52c1 Merge pull request #965 from freqtrade/fix/fix_959
catch crash with cobinhood
2018-07-01 14:28:01 +02:00
Nullart
8f49d5eb10 documentation updates 2018-06-30 19:32:56 +02:00
xmatthias
9e3e900f78 Add get_markets mock to new tests 2018-06-30 17:49:46 +02:00
xmatthias
14e12bd3c0 Fix missing comma in example.json 2018-06-30 17:37:34 +02:00
Samuel Husso
c29163a51c Merge pull request #970 from freqtrade/pyup-scheduled-update-2018-06-30
Scheduled daily dependency update on saturday
2018-06-30 09:37:38 -05:00
pyup-bot
5a591e01c0 Update sqlalchemy from 1.2.8 to 1.2.9 2018-06-30 14:23:07 +02:00
pyup-bot
c447644fd1 Update ccxt from 1.14.295 to 1.14.301 2018-06-30 14:23:06 +02:00
Nullart
98108a78f1 separating unfulfilled timeouts for buy and sell 2018-06-30 13:44:42 +02:00
Janne Sinivirta
0ce08932ed mypy fixes 2018-06-30 09:54:31 +03:00
Michael Egger
6dd5f85fb6 Merge pull request #954 from freqtrade/feat/allow_backtest_plot
allow backtest ploting
2018-06-29 19:44:06 +02:00
Samuel Husso
d8f2a683c6 Merge pull request #967 from freqtrade/pyup-scheduled-update-2018-06-29
Scheduled daily dependency update on friday
2018-06-29 08:32:34 -05:00
pyup-bot
8a941f3aa8 Update ccxt from 1.14.289 to 1.14.295 2018-06-29 14:23:06 +02:00
xmatthias
cf6b1a637a increase exchange code coverage 2018-06-28 22:32:28 +02:00
xmatthias
dcdc18a338 rename test-function 2018-06-28 22:18:38 +02:00
xmatthias
15c7854e7f add test for exchange_has 2018-06-28 22:11:45 +02:00
xmatthias
fe8a21681e add test for Not supported 2018-06-28 21:56:37 +02:00
xmatthias
ebbfc720b2 increase test coverage 2018-06-28 21:51:59 +02:00
xmatthias
8ec9a09749 Standardize retrier exception testing 2018-06-28 21:22:43 +02:00
xmatthias
2d4ce593b5 catch crash with cobinhood
fixes #959
2018-06-28 19:53:51 +02:00
Matthias
c5a00b4d45 Merge pull request #964 from freqtrade/pyup-scheduled-update-2018-06-28
Scheduled daily dependency update on thursday
2018-06-28 14:42:55 +02:00
pyup-bot
7cecae5279 Update ccxt from 1.14.288 to 1.14.289 2018-06-28 14:23:07 +02:00
xmatthias
d5ad066f8d support multiple db transitions by keeping the backup-table dynamic 2018-06-27 20:15:25 +02:00
xmatthias
860b270e30 update db migrate script to work for more changes 2018-06-27 19:49:08 +02:00
Samuel Husso
35e07bf11e Merge pull request #962 from freqtrade/pyup-scheduled-update-2018-06-27
Scheduled daily dependency update on wednesday
2018-06-27 08:40:39 -05:00
pyup-bot
19beb0941f Update ccxt from 1.14.272 to 1.14.288 2018-06-27 14:23:07 +02:00
xmatthias
8ecdae67e1 add mypy ignore (and comment as to why) 2018-06-27 06:57:41 +02:00
xmatthias
e6e868a03c remove markdown code type as it is not valid json 2018-06-27 06:54:29 +02:00
xmatthias
78e6c9fdf6 add tests for trailing stoploss 2018-06-27 06:52:31 +02:00
xmatthias
c997aa9864 move initial logic to persistence 2018-06-27 06:38:49 +02:00
xmatthias
a91d75b3b2 Add test for adjust_stop-loss 2018-06-27 06:23:49 +02:00
xmatthias
e9d5bceeb9 cleanly check if stop_loss is initialized 2018-06-27 00:18:50 +02:00
xmatthias
88b898cce4 add test for moving stoploss 2018-06-27 00:18:30 +02:00
xmatthias
8bec505bbe add test for trailing_stoploss 2018-06-26 23:40:36 +02:00
xmatthias
a3708bc56e add missing test 2018-06-26 23:40:20 +02:00
xmatthias
03005bc0f1 update documentation 2018-06-26 23:14:12 +02:00
xmatthias
da5be9fbd0 add stop_loss based on work from @berlinguyinca 2018-06-26 23:06:27 +02:00
xmatthias
3e167e1170 update sample configs 2018-06-26 22:41:38 +02:00
xmatthias
5015bc9bb0 slight update to persistence 2018-06-26 22:41:28 +02:00
xmatthias
243c36b39b get persistence.py for stop_loss 2018-06-26 20:49:07 +02:00
xmatthias
9ac3c559b6 fix some stoploss documentation 2018-06-26 20:30:16 +02:00
peterkorodi
257e1847b1 Update stoploss.md 2018-06-26 20:30:10 +02:00
Gert Wohlgemuth
54f52fb366 Create stoploss.md 2018-06-26 20:30:03 +02:00
Matthias
e1d8a59b69 Merge pull request #960 from freqtrade/pyup-scheduled-update-2018-06-26
Scheduled daily dependency update on tuesday
2018-06-26 14:43:31 +02:00
pyup-bot
7c2a50cef9 Update ccxt from 1.14.267 to 1.14.272 2018-06-26 14:23:06 +02:00
Samuel Husso
4c7d1c90db Merge pull request #957 from freqtrade/pyup-scheduled-update-2018-06-25
Scheduled daily dependency update on monday
2018-06-25 08:15:30 -05:00
pyup-bot
4f1fa28658 Update ccxt from 1.14.257 to 1.14.267 2018-06-25 14:23:06 +02:00
Janne Sinivirta
2b6407e598 remove unused tests from hyperopt 2018-06-25 11:38:42 +03:00
Janne Sinivirta
0bddc58ec4 extract loading previous results to a method 2018-06-25 11:38:14 +03:00
Janne Sinivirta
17ee7f8be5 fix typo in requirements.txt 2018-06-25 11:15:11 +03:00
Michael Egger
375ea940f4 Merge pull request #956 from freqtrade/fix/download_backtest
slight rework of download script
2018-06-24 21:44:09 +02:00
xmatthias
43f1a1d264 rework download_backtest script 2018-06-24 19:52:12 +02:00
xmatthias
e70cb963f7 document what to do with exported backtest results 2018-06-24 17:00:00 +02:00
Samuel Husso
a8cb0b0321 Merge pull request #955 from freqtrade/pyup-scheduled-update-2018-06-24
Scheduled daily dependency update on sunday
2018-06-24 08:01:04 -05:00
Janne Sinivirta
118a43cbb8 fixing tests for hyperopt 2018-06-24 15:27:53 +03:00
pyup-bot
5e7e977ffa Update ccxt from 1.14.256 to 1.14.257 2018-06-24 14:23:05 +02:00
xmatthias
660ec6f443 fix parameter type 2018-06-24 13:43:27 +02:00
gcarq
e98f22ef2f Merge branch 'master' of https://github.com/freqtrade/freqtrade into develop 2018-06-24 00:39:11 +02:00
Samuel Husso
1529ce8bdb Merge pull request #952 from freqtrade/bump-version
bump develop to 0.17.1
2018-06-23 16:21:56 -05:00
xmatthias
d8cb63efdd extract load_trades 2018-06-23 20:19:07 +02:00
xmatthias
5055563458 add --plot-limit 2018-06-23 20:14:15 +02:00
xmatthias
f506ebcd62 use Pathlib in the whole script 2018-06-23 19:58:28 +02:00
xmatthias
3cedace2f6 add plotting for backtested trades 2018-06-23 19:54:27 +02:00
Samuel Husso
3384679bad bump develop to 0.17.1 2018-06-23 09:38:20 -05:00
Janne Sinivirta
642ad02316 remove unused import 2018-06-23 15:56:38 +03:00
Janne Sinivirta
ab9e2fcea0 fix guard names to match search space 2018-06-23 15:47:19 +03:00
Janne Sinivirta
136456afc0 add three triggers to hyperopting 2018-06-23 15:44:51 +03:00
Janne Sinivirta
09261b11af remove hyperopt and networkx from dependencies 2018-06-23 15:22:14 +03:00
xmatthias
0440a19171 export open/close rate for backtesting too
preparation to allow plotting of backtest results
2018-06-23 14:19:50 +02:00
Janne Sinivirta
e8f2e6956d to avoid pickle problems, get rid of reference to exchange after initialization 2018-06-23 14:37:36 +03:00
Janne Sinivirta
dde7df7fd3 add scikit-optimize to dependencies 2018-06-23 14:37:36 +03:00
Janne Sinivirta
a525cba8e9 switch signal handler to try catch. fix pickling and formatting output 2018-06-23 14:37:36 +03:00
Janne Sinivirta
8272120c3a convert stoploss and ROI search spaces to skopt format 2018-06-23 14:37:36 +03:00
Janne Sinivirta
8fee2e2409 move result logging out from optimizer 2018-06-23 14:37:36 +03:00
Janne Sinivirta
c415014153 use multiple jobs in acq 2018-06-23 14:37:36 +03:00
Janne Sinivirta
964cbdc262 increase initial sampling points 2018-06-23 14:37:36 +03:00
Janne Sinivirta
a46badd5c0 reuse pool workers 2018-06-23 14:37:36 +03:00
Janne Sinivirta
0cb1aedf5b problem with pickling 2018-06-23 14:37:36 +03:00
Janne Sinivirta
b485e6e0ba start small 2018-06-23 14:37:36 +03:00
gcarq
810d7de869 tests: add dir() assertion 2018-06-23 14:37:36 +03:00
gcarq
398b21a11d implement test for import_strategy 2018-06-23 14:37:36 +03:00
gcarq
78f50a1471 move logic from hyperopt to freqtrade.strategy 2018-06-23 14:37:36 +03:00
gcarq
5aae215c94 wrap strategies with HyperoptStrategy for module lookups with pickle 2018-06-23 14:37:36 +03:00
xmatthias
2738d3aed8 update plotly 2018-06-23 14:37:36 +03:00
Janne Sinivirta
01d45bee76 fix flake8 2018-06-23 14:37:36 +03:00
Janne Sinivirta
c1691f21f3 check that we set fee on backtesting init 2018-06-23 14:37:36 +03:00
Janne Sinivirta
a68c90c512 avoid calling exchange.get_fee inside loop 2018-06-23 14:37:36 +03:00
Pan Long
e759a90b2d Update doc for manually fix trade
The profit should be close_rate/open_rate-1   not close_rate/open_rate
2018-06-22 19:16:48 +05:30
81 changed files with 6190 additions and 3555 deletions

View File

@@ -13,12 +13,12 @@ addons:
install: install:
- ./install_ta-lib.sh - ./install_ta-lib.sh
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install --upgrade flake8 coveralls pytest-random-order mypy - pip install --upgrade flake8 coveralls pytest-random-order pytest-asyncio mypy
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install -e . - pip install -e .
jobs: jobs:
include: include:
- script: - script:
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ - pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
- coveralls - coveralls
- script: - script:

View File

@@ -1,10 +1,11 @@
FROM python:3.6.5-slim-stretch FROM python:3.7.0-slim-stretch
# Install TA-lib # Install TA-lib
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean RUN apt-get update && apt-get -y install curl build-essential && apt-get clean
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \ RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
tar xzvf - && \ tar xzvf - && \
cd ta-lib && \ cd ta-lib && \
sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && \
./configure && make && make install && \ ./configure && make && make install && \
cd .. && rm -rf ta-lib cd .. && rm -rf ta-lib
ENV LD_LIBRARY_PATH /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib
@@ -15,9 +16,10 @@ WORKDIR /freqtrade
# Install dependencies # Install dependencies
COPY requirements.txt /freqtrade/ COPY requirements.txt /freqtrade/
RUN pip install -r requirements.txt RUN pip install numpy --no-cache-dir \
&& pip install -r requirements.txt --no-cache-dir
# Install and execute # Install and execute
COPY . /freqtrade/ COPY . /freqtrade/
RUN pip install -e . RUN pip install -e . --no-cache-dir
ENTRYPOINT ["freqtrade"] ENTRYPOINT ["freqtrade"]

View File

@@ -4,13 +4,12 @@
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
Simple High frequency trading bot for crypto currencies designed to support multi exchanges and be controlled via Telegram.
Simple High frequency trading bot for crypto currencies designed to
support multi exchanges and be controlled via Telegram.
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
## Disclaimer ## Disclaimer
This software is for educational purposes only. Do not risk money which This software is for educational purposes only. Do not risk money which
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
@@ -23,18 +22,18 @@ We strongly recommend you to have coding and Python knowledge. Do not
hesitate to read the source code and understand the mechanism of this bot. hesitate to read the source code and understand the mechanism of this bot.
## Exchange marketplaces supported ## Exchange marketplaces supported
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/) - [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance))
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
## Features ## Features
- [x] **Based on Python 3.6+**: For botting on any operating system -
Windows, macOS and Linux - [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux
- [x] **Persistence**: Persistence is achieved through sqlite - [x] **Persistence**: Persistence is achieved through sqlite
- [x] **Dry-run**: Run the bot without playing money. - [x] **Dry-run**: Run the bot without playing money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Backtesting**: Run a simulation of your buy/sell strategy.
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data.
strategy parameters with real exchange data.
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade. - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade.
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
- [x] **Manageable via Telegram**: Manage the bot with Telegram - [x] **Manageable via Telegram**: Manage the bot with Telegram
@@ -43,38 +42,45 @@ strategy parameters with real exchange data.
- [x] **Performance status report**: Provide a performance status of your current trades. - [x] **Performance status report**: Provide a performance status of your current trades.
## Table of Contents ## Table of Contents
- [Quick start](#quick-start) - [Quick start](#quick-start)
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) - [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) - [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) - [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) - [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)
- [Basic Usage](#basic-usage) - [Basic Usage](#basic-usage)
- [Bot commands](#bot-commands) - [Bot commands](#bot-commands)
- [Telegram RPC commands](#telegram-rpc-commands) - [Telegram RPC commands](#telegram-rpc-commands)
- [Support](#support) - [Support](#support)
- [Help](#help--slack) - [Help](#help--slack)
- [Bugs](#bugs--issues) - [Bugs](#bugs--issues)
- [Feature Requests](#feature-requests) - [Feature Requests](#feature-requests)
- [Pull Requests](#pull-requests) - [Pull Requests](#pull-requests)
- [Requirements](#requirements) - [Requirements](#requirements)
- [Min hardware required](#min-hardware-required) - [Min hardware required](#min-hardware-required)
- [Software requirements](#software-requirements) - [Software requirements](#software-requirements)
## Quick start ## Quick start
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.
```bash ```bash
git clone git@github.com:freqtrade/freqtrade.git git clone git@github.com:freqtrade/freqtrade.git
git checkout develop
cd freqtrade cd freqtrade
git checkout develop
./setup.sh --install ./setup.sh --install
``` ```
_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_ _Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_
## Documentation ## Documentation
We invite you to read the bot documentation to ensure you understand how the bot is working. We invite you to read the bot documentation to ensure you understand how the bot is working.
- [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) - [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) - [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) - [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
@@ -86,7 +92,6 @@ We invite you to read the bot documentation to ensure you understand how the bot
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
## Basic Usage ## Basic Usage
### Bot commands ### Bot commands
@@ -125,17 +130,15 @@ optional arguments:
``` ```
### Telegram RPC commands ### Telegram RPC commands
Telegram is not mandatory. However, this is a great way to control your
bot. More details on our Telegram is not mandatory. However, this is a great way to control your bot. More details on our [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
[documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
- `/start`: Starts the trader - `/start`: Starts the trader
- `/stop`: Stops the trader - `/stop`: Stops the trader
- `/status [table]`: Lists all open trades - `/status [table]`: Lists all open trades
- `/count`: Displays number of open trades - `/count`: Displays number of open trades
- `/profit`: Lists cumulative profit from all finished trades - `/profit`: Lists cumulative profit from all finished trades
- `/forcesell <trade_id>|all`: Instantly sells the given trade - `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`).
(Ignoring `minimum_roi`).
- `/performance`: Show performance of each finished trade grouped by pair - `/performance`: Show performance of each finished trade grouped by pair
- `/balance`: Show account balance per currency - `/balance`: Show account balance per currency
- `/daily <n>`: Shows profit or loss per day, over the last n days - `/daily <n>`: Shows profit or loss per day, over the last n days
@@ -144,20 +147,30 @@ bot. More details on our
## Development branches ## Development branches
The project is currently setup in two main branches:
- `develop` - This branch has often new features, but might also cause
breaking changes.
- `master` - This branch contains the latest stable release. The bot
'should' be stable on this branch, and is generally well tested.
The project is currently setup in two main branches:
- `develop` - This branch has often new features, but might also cause breaking changes.
- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested.
- `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature.
## A note on Binance
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore.
## Support ## Support
### Help / Slack ### Help / Slack
For any questions not covered by the documentation or for further For any questions not covered by the documentation or for further
information about the bot, we encourage you to join our slack channel. information about the bot, we encourage you to join our slack channel.
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). - [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
If you discover a bug in the bot, please If you discover a bug in the bot, please
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
first. If it hasn't been reported, please first. If it hasn't been reported, please
@@ -166,6 +179,7 @@ ensure you follow the template guide so that our team can assist you as
quickly as possible. quickly as possible.
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) ### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
Have you a great idea to improve the bot you want to share? Please, Have you a great idea to improve the bot you want to share? Please,
first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
If it hasn't been requested, please If it hasn't been requested, please
@@ -174,6 +188,7 @@ and ensure you follow the template guide so that it does not get lost
in the bug reports. in the bug reports.
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls) ### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
Feel like our bot is missing a feature? We welcome your pull requests! Feel like our bot is missing a feature? We welcome your pull requests!
Please read our Please read our
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) [Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
@@ -181,16 +196,22 @@ to understand the requirements before sending your pull-requests.
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
**Important:** Always create your PR against the `develop` branch, not **Important:** Always create your PR against the `develop` branch, not `master`.
`master`.
## Requirements ## Requirements
### Uptodate clock
The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges.
### Min hardware required ### Min hardware required
To run this bot we recommend you a cloud instance with a minimum of: To run this bot we recommend you a cloud instance with a minimum of:
* Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU
- Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU
### Software requirements ### Software requirements
- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) - [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
- [pip](https://pip.pypa.io/en/stable/installing/) - [pip](https://pip.pypa.io/en/stable/installing/)
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)

View File

@@ -5,14 +5,30 @@
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"ticker_interval" : "5m", "ticker_interval" : "5m",
"dry_run": false, "dry_run": false,
"unfilledtimeout": 600, "trailing_stop": false,
"unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0,
"use_order_book": false,
"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
}, },
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"ccxt_rate_limit": true,
"pair_whitelist": [ "pair_whitelist": [
"ETH/BTC", "ETH/BTC",
"LTC/BTC", "LTC/BTC",

View File

@@ -5,6 +5,9 @@
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"dry_run": false, "dry_run": false,
"ticker_interval": "5m", "ticker_interval": "5m",
"trailing_stop": false,
"trailing_stop_positive": 0.005,
"trailing_stop_positive_offset": 0.0051,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
"30": 0.01, "30": 0.01,
@@ -12,14 +15,29 @@
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.10, "stoploss": -0.10,
"unfilledtimeout": 600, "unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0,
"use_order_book": false,
"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
}, },
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
"key": "your_exchange_key", "key": "your_exchange_key",
"secret": "your_exchange_secret", "secret": "your_exchange_secret",
"ccxt_rate_limit": true,
"pair_whitelist": [ "pair_whitelist": [
"ETH/BTC", "ETH/BTC",
"LTC/BTC", "LTC/BTC",
@@ -34,7 +52,8 @@
], ],
"pair_blacklist": [ "pair_blacklist": [
"DOGE/BTC" "DOGE/BTC"
] ],
"outdated_offset": 5
}, },
"experimental": { "experimental": {
"use_sell_signal": false, "use_sell_signal": false,

View File

@@ -29,25 +29,25 @@ The backtesting is very easy with freqtrade.
#### With 5 min tickers (Per default) #### With 5 min tickers (Per default)
```bash ```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation python3 ./freqtrade/main.py backtesting
``` ```
#### With 1 min tickers #### With 1 min tickers
```bash ```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m python3 ./freqtrade/main.py backtesting --ticker-interval 1m
``` ```
#### Update cached pairs with the latest data #### Update cached pairs with the latest data
```bash ```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached python3 ./freqtrade/main.py backtesting --refresh-pairs-cached
``` ```
#### With live data (do not alter your testdata files) #### With live data (do not alter your testdata files)
```bash ```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --live python3 ./freqtrade/main.py backtesting --live
``` ```
#### Using a different on-disk ticker-data source #### Using a different on-disk ticker-data source
@@ -70,6 +70,36 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_
python3 ./freqtrade/main.py backtesting --export trades python3 ./freqtrade/main.py backtesting --export trades
``` ```
The exported trades can be read using the following code for manual analysis, or can be used by the plotting script `plot_dataframe.py` in the scripts folder.
``` python
import json
from pathlib import Path
import pandas as pd
filename=Path('user_data/backtest_data/backtest-result.json')
with filename.open() as file:
data = json.load(file)
columns = ["pair", "profit", "opents", "closets", "index", "duration",
"open_rate", "close_rate", "open_at_end", "sell_reason"]
df = pd.DataFrame(data, columns=columns)
df['opents'] = pd.to_datetime(df['opents'],
unit='s',
utc=True,
infer_datetime_format=True
)
df['closets'] = pd.to_datetime(df['closets'],
unit='s',
utc=True,
infer_datetime_format=True
)
```
If you have some ideas for interesting / helpful backtest data analysis, feel free to submit a PR so the community can benefit from it.
#### Exporting trades to file specifying a custom filename #### Exporting trades to file specifying a custom filename
```bash ```bash
@@ -121,7 +151,7 @@ cp freqtrade/tests/testdata/pairs.json user_data/data/binance
Then run: Then run:
```bash ```bash
python scripts/download_backtest_data --exchange binance python scripts/download_backtest_data.py --exchange binance
``` ```
This will download ticker data for all the currency pairs you defined in `pairs.json`. This will download ticker data for all the currency pairs you defined in `pairs.json`.
@@ -208,6 +238,31 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
profit. Hence, keep in mind that your performance is a mix of your profit. Hence, keep in mind that your performance is a mix of your
strategies, your configuration, and the crypto-currency you have set up. strategies, your configuration, and the crypto-currency you have set up.
## Backtesting multiple strategies
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
strategies you'd like to compare, this should give a nice runtime boost.
All listed Strategies need to be in the same folder.
``` bash
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.
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.
```
=================================================== Strategy Summary ====================================================
| Strategy | buy count | avg profit % | cum profit % | total profit ETH | avg duration | profit | loss |
|:-----------|------------:|---------------:|---------------:|-------------------:|:----------------|---------:|-------:|
| Strategy1 | 19 | -0.76 | -14.39 | -0.01440287 | 15:48:00 | 15 | 4 |
| Strategy2 | 6 | -2.73 | -16.40 | -0.01641299 | 1 day, 14:12:00 | 3 | 3 |
```
## Next step ## Next step
Great, your strategy is profitable. What if the bot can give your the Great, your strategy is profitable. What if the bot can give your the

View File

@@ -39,7 +39,6 @@ A strategy file contains all the information needed to build a good strategy:
- Sell strategy rules - Sell strategy rules
- Minimal ROI recommended - Minimal ROI recommended
- Stoploss recommended - Stoploss recommended
- Hyperopt parameter
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`. The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`.
You can test it with the parameter: `--strategy TestStrategy` You can test it with the parameter: `--strategy TestStrategy`
@@ -61,22 +60,22 @@ file as reference.**
### Buy strategy ### Buy strategy
Edit the method `populate_buy_trend()` into your strategy file to Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy.
update your buy strategy.
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/test_strategy.py`:
```python ```python
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 30) & (dataframe['adx'] > 30) &
(dataframe['tema'] <= dataframe['blower']) & (dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1)) (dataframe['tema'] > dataframe['tema'].shift(1))
), ),
'buy'] = 1 'buy'] = 1
@@ -87,38 +86,47 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
### Sell strategy ### Sell strategy
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy. Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration.
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/test_strategy.py`:
```python ```python
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 70) & (dataframe['adx'] > 70) &
(dataframe['tema'] > dataframe['blower']) & (dataframe['tema'] > dataframe['bb_middleband']) &
(dataframe['tema'] < dataframe['tema'].shift(1)) (dataframe['tema'] < dataframe['tema'].shift(1))
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
``` ```
## Add more Indicator ## Add more Indicators
As you have seen, buy and sell strategies need indicators. You can add As you have seen, buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
more indicators by extending the list contained in
the method `populate_indicators()` from your strategy file. You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer.
Sample: Sample:
```python ```python
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
""" """
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
@@ -149,6 +157,11 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
return dataframe return dataframe
``` ```
### Metadata dict
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
### Want more indicator examples ### Want more indicator examples
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py). Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).

View File

@@ -1,13 +1,15 @@
# Bot usage # Bot usage
This page explains the difference parameters of the bot and how to run
it. This page explains the difference parameters of the bot and how to run it.
## Table of Contents ## Table of Contents
- [Bot commands](#bot-commands) - [Bot commands](#bot-commands)
- [Backtesting commands](#backtesting-commands) - [Backtesting commands](#backtesting-commands)
- [Hyperopt commands](#hyperopt-commands) - [Hyperopt commands](#hyperopt-commands)
## Bot commands ## Bot commands
``` ```
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]] [--strategy-path PATH] [--dynamic-whitelist [INT]]
@@ -41,6 +43,7 @@ optional arguments:
``` ```
### How to use a different config file? ### How to use a different config file?
The bot allows you to select which config file you want to use. Per The bot allows you to select which config file you want to use. Per
default, the bot will load the file `./config.json` default, the bot will load the file `./config.json`
@@ -49,6 +52,7 @@ python3 ./freqtrade/main.py -c path/far/far/away/config.json
``` ```
### 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.
Per default without `--strategy` or `-s` the bot will load the Per default without `--strategy` or `-s` the bot will load the
`DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`). `DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`).
@@ -60,6 +64,7 @@ To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this
**Example:** **Example:**
In `user_data/strategies` you have a file `my_awesome_strategy.py` which has 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/main.py --strategy AwesomeStrategy python3 ./freqtrade/main.py --strategy AwesomeStrategy
``` ```
@@ -70,6 +75,7 @@ message the reason (File not found, or errors in your code).
Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md). Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).
### 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 folder!):
```bash ```bash
@@ -77,21 +83,25 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/fol
``` ```
#### 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 folder
`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? ### How to use --dynamic-whitelist?
Per default `--dynamic-whitelist` will retrieve the 20 currencies based Per default `--dynamic-whitelist` will retrieve the 20 currencies based
on BaseVolume. This value can be changed when you run the script. on BaseVolume. This value can be changed when you run the script.
**By Default** **By Default**
Get the 20 currencies based on BaseVolume. Get the 20 currencies based on BaseVolume.
```bash ```bash
python3 ./freqtrade/main.py --dynamic-whitelist python3 ./freqtrade/main.py --dynamic-whitelist
``` ```
**Customize the number of currencies to retrieve** **Customize the number of currencies to retrieve**
Get the 30 currencies based on BaseVolume. Get the 30 currencies based on BaseVolume.
```bash ```bash
python3 ./freqtrade/main.py --dynamic-whitelist 30 python3 ./freqtrade/main.py --dynamic-whitelist 30
``` ```
@@ -102,6 +112,7 @@ negative value (e.g -2), `--dynamic-whitelist` will use the default
value (20). value (20).
### 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
stored in a database. If you want to store your bot actions in a DB stored in a database. If you want to store your bot actions in a DB
using `--db-url`. This can also be used to specify a custom database using `--db-url`. This can also be used to specify a custom database
@@ -111,24 +122,27 @@ in production mode. Example command:
python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
``` ```
## Backtesting commands ## Backtesting commands
Backtesting also uses the config specified via `-c/--config`. Backtesting also uses the config specified via `-c/--config`.
``` ```
usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--realistic-simulation] usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp]
[--timerange TIMERANGE] [-l] [-r] [--export EXPORT] [--timerange TIMERANGE] [-l] [-r]
[--export-filename EXPORTFILENAME] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH]
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)
--realistic-simulation --eps, --enable-position-stacking
uses max_open_trades from config to simulate real Allow buying the same pair multiple times (position
world limitations stacking)
--dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high
number)
--timerange TIMERANGE --timerange TIMERANGE
specify what timerange of data to use. specify what timerange of data to use.
-l, --live using live data -l, --live using live data
@@ -136,16 +150,26 @@ optional arguments:
refresh the pairs files in tests/testdata with the refresh the pairs files in tests/testdata with the
latest data from the exchange. Use it if you want to latest data from the exchange. Use it if you want to
run your backtesting with up-to-date data. run your backtesting with up-to-date data.
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a commaseparated 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
--export EXPORT export backtest results, argument are: trades Example --export EXPORT export backtest results, argument are: trades Example
--export=trades --export=trades
--export-filename EXPORTFILENAME --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=backtest_today.json (default: backtest- filename=user_data/backtest_data/backtest_today.json
result.json (default: user_data/backtest_data/backtest-
result.json)
``` ```
### How to use --refresh-pairs-cached parameter? ### How to use --refresh-pairs-cached parameter?
The first time your run Backtesting, it will take the pairs you have The first time your run Backtesting, it will take the pairs you have
set in your config file and download data from Bittrex. set in your config file and download data from Bittrex.
@@ -157,36 +181,42 @@ to come back to the previous version.**
To test your strategy with latest data, we recommend continuing using To test your strategy with latest data, we recommend continuing using
the parameter `-l` or `--live`. the parameter `-l` or `--live`.
## Hyperopt commands ## Hyperopt commands
To optimize your strategy, you can use hyperopt parameter hyperoptimization To optimize your strategy, you can use hyperopt parameter hyperoptimization
to find optimal parameter values for your stategy. to find optimal parameter values for your stategy.
``` ```
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation] usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp]
[--timerange TIMERANGE] [-e INT] [--timerange TIMERANGE] [-e INT]
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]] [-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
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)
--realistic-simulation --eps, --enable-position-stacking
uses max_open_trades from config to simulate real Allow buying the same pair multiple times (position
world limitations stacking)
--timerange TIMERANGE specify what timerange of data to use. --dmmp, --disable-max-market-positions
Disable applying `max_open_trades` during backtest
(same as setting `max_open_trades` to a very high
number)
--timerange TIMERANGE
specify what timerange of data to use.
-e INT, --epochs INT specify number of epochs (default: 100) -e INT, --epochs INT specify number of epochs (default: 100)
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...] -s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
Specify which parameters to hyperopt. Space separate Specify which parameters to hyperopt. Space separate
list. Default: all list. Default: all
``` ```
## A parameter missing in the configuration? ## A parameter missing in the configuration?
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84) 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
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md). [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).

View File

@@ -1,12 +1,15 @@
# Configure the bot # Configure the bot
This page explains how to configure your `config.json` file. This page explains how to configure your `config.json` file.
## Table of Contents ## Table of Contents
- [Bot commands](#bot-commands) - [Bot commands](#bot-commands)
- [Backtesting commands](#backtesting-commands) - [Backtesting commands](#backtesting-commands)
- [Hyperopt commands](#hyperopt-commands) - [Hyperopt commands](#hyperopt-commands)
## Setup config.json ## Setup config.json
We recommend to copy and use the `config.json.example` as a template We recommend to copy and use the `config.json.example` as a template
for your bot configuration. for your bot configuration.
@@ -19,32 +22,50 @@ The table below will list all configuration parameters.
| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to 'unlimited' to allow the bot to use all avaliable balance. | `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to 'unlimited' to allow the bot to use all avaliable balance.
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes | `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below. | `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
| `process_only_new_candles` | false | No | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. Can be set either in Configuration or in the strategy.
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled. | `trailing_stop` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file).
| `trailing_stop_positve` | 0 | No | Changes stop-loss once profit has been reached.
| `trailing_stop_positve_offset` | 0 | No | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive.
| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
| `bid_strategy.use_order_book` | false | No | Allows buying of pair using the rates in Order Book Bids.
| `bid_strategy.order_book_top` | 0 | No | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
| `bid_strategy.check_depth_of_market.enabled` | false | No | Does not buy if the % difference of buy orders and sell orders is met in Order Book.
| `bid_strategy.check_depth_of_market.bids_to_ask_delta` | 0 | No | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher.
| `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks.
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode. | `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode. | `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. | `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. | `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
| `exchange.ccxt_rate_limit` | True | No | Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange.
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`. | `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision. | `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal` | `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram. | `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
| `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
| `webhook.enabled` | false | No | Enable useage of Webhook notifications
| `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
| `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
| `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`. | `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`.
| `initial_state` | running | No | Defines the initial application state. More information below. | `initial_state` | running | No | Defines the initial application state. More information below.
| `strategy` | DefaultStrategy | No | Defines Strategy class to use. | `strategy` | DefaultStrategy | No | Defines Strategy class to use.
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder). | `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second. | `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
The definition of each config parameters is in The definition of each config parameters is in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
### Understand stake_amount ### Understand stake_amount
`stake_amount` is an amount of crypto-currency your bot will use for each trade. `stake_amount` is an amount of crypto-currency your bot will use for each trade.
The minimal value is 0.0005. If there is not enough crypto-currency in The minimal value is 0.0005. If there is not enough crypto-currency in
the account an exception is generated. the account an exception is generated.
@@ -52,9 +73,11 @@ To allow the bot to trade all the avaliable `stake_currency` in your account set
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`. In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
### Understand minimal_roi ### Understand minimal_roi
`minimal_roi` is a JSON object where the key is a duration `minimal_roi` is a JSON object where the key is a duration
in minutes and the value is the minimum ROI in percent. in minutes and the value is the minimum ROI in percent.
See the example below: See the example below:
``` ```
"minimal_roi": { "minimal_roi": {
"40": 0.0, # Sell after 40 minutes if the profit is not negative "40": 0.0, # Sell after 40 minutes if the profit is not negative
@@ -69,6 +92,7 @@ value. This parameter is optional. If you use it, it will take over the
`minimal_roi` value from the strategy file. `minimal_roi` value from the strategy file.
### Understand stoploss ### Understand stoploss
`stoploss` is loss in percentage that should trigger a sale. `stoploss` is loss in percentage that should trigger a sale.
For example value `-0.10` will cause immediate sell if the For example value `-0.10` will cause immediate sell if the
profit dips below -10% for a given trade. This parameter is optional. profit dips below -10% for a given trade. This parameter is optional.
@@ -77,82 +101,100 @@ Most of the strategy files already include the optimal `stoploss`
value. This parameter is optional. If you use it, it will take over the value. This parameter is optional. If you use it, it will take over the
`stoploss` value from the strategy file. `stoploss` value from the strategy file.
### Understand trailing stoploss
Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing stoploss.
### Understand initial_state ### Understand initial_state
`initial_state` is an optional field that defines the initial application state. `initial_state` is an optional field that defines the initial application state.
Possible values are `running` or `stopped`. (default=`running`) Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first. If the value is `stopped` the bot has to be started with `/start` first.
### Understand process_throttle_secs ### Understand process_throttle_secs
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait `process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
the static list of pairs) if we should buy. the static list of pairs) if we should buy.
### Understand ask_last_balance ### Understand ask_last_balance
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will `ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
use the `last` price and values between those interpolate between ask and last use the `last` price and values between those interpolate between ask and last
price. Using `ask` price will guarantee quick success in bid, but bot will also price. Using `ask` price will guarantee quick success in bid, but bot will also
end up paying more then would probably have been necessary. end up paying more then would probably have been necessary.
### What values for exchange.name? ### What values for exchange.name?
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
exchange markets and trading APIs. The complete up-to-date list can be found in the exchange markets and trading APIs. The complete up-to-date list can be found in the
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
with only Bittrex and Binance. with only Bittrex and Binance.
The bot was tested with the following exchanges: The bot was tested with the following exchanges:
- [Bittrex](https://bittrex.com/): "bittrex" - [Bittrex](https://bittrex.com/): "bittrex"
- [Binance](https://www.binance.com/): "binance" - [Binance](https://www.binance.com/): "binance"
Feel free to test other exchanges and submit your PR to improve the bot. Feel free to test other exchanges and submit your PR to improve the bot.
### What values for fiat_display_currency? ### What values for fiat_display_currency?
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram. `fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD". The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
In addition to central bank currencies, a range of cryto currencies are supported. In addition to central bank currencies, a range of cryto currencies are supported.
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT". The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
## Switch to dry-run mode ## Switch to dry-run mode
We recommend starting the bot in dry-run mode to see how your bot will We recommend starting the bot in dry-run mode to see how your bot will
behave and how is the performance of your strategy. In Dry-run mode the behave and how is the performance of your strategy. In Dry-run mode the
bot does not engage your money. It only runs a live simulation without bot does not engage your money. It only runs a live simulation without
creating trades. creating trades.
### To switch your bot in Dry-run mode: ### To switch your bot in Dry-run mode:
1. Edit your `config.json` file 1. Edit your `config.json` file
2. Switch dry-run to true and specify db_url for a persistent db 2. Switch dry-run to true and specify db_url for a persistent db
```json ```json
"dry_run": true, "dry_run": true,
"db_url": "sqlite///tradesv3.dryrun.sqlite", "db_url": "sqlite///tradesv3.dryrun.sqlite",
``` ```
3. Remove your Exchange API key (change them by fake api credentials) 3. Remove your Exchange API key (change them by fake api credentials)
```json ```json
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
"key": "key", "key": "key",
"secret": "secret", "secret": "secret",
... ...
} }
``` ```
Once you will be happy with your bot performance, you can switch it to Once you will be happy with your bot performance, you can switch it to
production mode. production mode.
## Switch to production mode ## Switch to production mode
In production mode, the bot will engage your money. Be careful a wrong In production mode, the bot will engage your money. Be careful a wrong
strategy can lose all your money. Be aware of what you are doing when strategy can lose all your money. Be aware of what you are doing when
you run it in production mode. you run it in production mode.
### To switch your bot in production mode: ### To switch your bot in production mode:
1. Edit your `config.json` file 1. Edit your `config.json` file
2. Switch dry-run to false and don't forget to adapt your database URL if set 2. Switch dry-run to false and don't forget to adapt your database URL if set
```json ```json
"dry_run": false, "dry_run": false,
``` ```
3. Insert your Exchange API key (change them by fake api keys) 3. Insert your Exchange API key (change them by fake api keys)
```json ```json
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
@@ -160,10 +202,37 @@ you run it in production mode.
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
... ...
} }
``` ```
If you have not your Bittrex API key yet, If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
[see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
### Embedding Strategies
FreqTrade provides you with with an easy way to embed the strategy into your configuration file.
This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field,
in your chosen config file.
##### Encoding a string as BASE64
This is a quick example, how to generate the BASE64 string in python
```python
from base64 import urlsafe_b64encode
with open(file, 'r') as f:
content = f.read()
content = urlsafe_b64encode(content.encode('utf-8'))
```
The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following
```json
"strategy": "NameOfStrategy:BASE64String"
```
Please ensure that 'NameOfStrategy' is identical to the strategy name!
## Next step ## Next step
Now you have configured your config.json, the next step is to
[start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md). Now you have configured your config.json, the next step is to [start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).

View File

@@ -1,155 +1,116 @@
# Hyperopt # Hyperopt
This page explains how to tune your strategy by finding the optimal This page explains how to tune your strategy by finding the optimal
parameters with Hyperopt. parameters, a process called hyperparameter optimization. The bot uses several
algorithms included in the `scikit-optimize` package to accomplish this. The
search will burn all your CPU cores, make your laptop sound like a fighter jet
and still take a long time.
*Note:* Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
## Table of Contents ## Table of Contents
- [Prepare your Hyperopt](#prepare-hyperopt) - [Prepare your Hyperopt](#prepare-hyperopt)
- [1. Configure your Guards and Triggers](#1-configure-your-guards-and-triggers) - [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
- [2. Update the hyperopt config file](#2-update-the-hyperopt-config-file) - [Solving a Mystery](#solving-a-mystery)
- [Advanced Hyperopt notions](#advanced-notions) - [Adding New Indicators](#adding-new-indicators)
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
- [Execute Hyperopt](#execute-hyperopt) - [Execute Hyperopt](#execute-hyperopt)
- [Understand the hyperopts result](#understand-the-backtesting-result) - [Understand the hyperopts result](#understand-the-backtesting-result)
## Prepare Hyperopt ## Prepare Hyperopting
Before we start digging in Hyperopt, we recommend you to take a look at We recommend you start by taking a look at `hyperopt.py` file located in [freqtrade/optimize](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
your strategy file located into [user_data/strategies/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
### 1. Configure your Guards and Triggers ### Configure your Guards and Triggers
There are two places you need to change in your strategy file to add a There are two places you need to change to add a new buy strategy for testing:
new buy strategy for testing: - Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L278-L294).
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294). - Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L218-L229)
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`. and the associated methods `indicator_space`, `roi_space`, `stoploss_space`.
There you have two different type of indicators: 1. `guards` and 2. There you have two different type of indicators: 1. `guards` and 2. `triggers`.
`triggers`. 1. Guards are conditions like "never buy if ADX < 10", or "never buy if
1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10".
current price is over EMA10.
2. Triggers are ones that actually trigger buy in specific moment, like 2. Triggers are ones that actually trigger buy in specific moment, like
"buy when EMA5 crosses over EMA10" or buy when close price touches lower "buy when EMA5 crosses over EMA10" or "buy when close price touches lower
bollinger band. bollinger band".
HyperOpt will, for each eval round, pick just ONE trigger, and possibly Hyperoptimization will, for each eval round, pick one trigger and possibly
multiple guards. So that the constructed strategy will be something like multiple guards. The constructed strategy will be something like
"*buy exactly when close price touches lower bollinger band, BUT only if "*buy exactly when close price touches lower bollinger band, BUT only if
ADX > 10*". ADX > 10*".
If you have updated the buy strategy, ie. changed the contents of
If you have updated the buy strategy, means change the content of
`populate_buy_trend()` method you have to update the `guards` and `populate_buy_trend()` method you have to update the `guards` and
`triggers` hyperopts must used. `triggers` hyperopts must use.
As for an example if your `populate_buy_trend()` method is: ## Solving a Mystery
```python
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(dataframe['rsi'] < 35) &
(dataframe['adx'] > 65),
'buy'] = 1
return dataframe Let's say you are curious: should you use MACD crossings or lower Bollinger
``` Bands to trigger your buys. And you also wonder should you use RSI or ADX to
help with those buy decisions. If you decide to use RSI or ADX, which values
should I use for them? So let's use hyperparameter optimization to solve this
mystery.
Your hyperopt file must contain `guards` to find the right value for We will start by defining a search space:
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
means you will need to enable/disable triggers.
In our case the `SPACE` and `populate_buy_trend` in your strategy file
will look like:
```python
space = {
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'trigger': hp.choice('trigger', [
{'type': 'lower_bb'},
{'type': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]),
}
...
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
# TRIGGERS
triggers = {
'lower_bb': dataframe['tema'] <= dataframe['blower'],
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
}
...
```
### 2. Update the hyperopt config file
Hyperopt is using a dedicated config file. Currently hyperopt
cannot use your config file. It is also made on purpose to allow you
testing your strategy with different configurations.
The Hyperopt configuration is located in
[user_data/hyperopt_conf.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopt_conf.py).
## Advanced notions
### Understand the Guards and Triggers
When you need to add the new guards and triggers to be hyperopt
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
If it's a guard, you will add a line like this:
```
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
```
This says, "*one of the guards is RSI, it can have two values, enabled or
disabled. If it is enabled, try different values for it between 20 and 40*".
So, the part of the strategy builder using the above setting looks like
this:
``` ```
if params['rsi']['enabled']: def indicator_space() -> List[Dimension]:
conditions.append(dataframe['rsi'] < params['rsi']['value']) """
Define your Hyperopt space for searching strategy parameters
"""
return [
Integer(20, 40, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal'], name='trigger')
]
``` ```
It checks if Hyperopt wants the RSI guard to be enabled for this Above definition says: I have five parameters I want you to randomly combine
round `params['rsi']['enabled']` and if it is, then it will add a to find the best combination. Two of them are integer values (`adx-value`
condition that says RSI must be smaller than the value hyperopt picked and `rsi-value`) and I want you test in the range of values 20 to 40.
for this evaluation, which is given in the `params['rsi']['value']`. Then we have three category variables. First two are either `True` or `False`.
We use these to either enable or disable the ADX and RSI guards. The last
one we call `trigger` and use it to decide which buy trigger we want to use.
That's it. Now you can add new parts of strategies to Hyperopt and it So let's write the buy strategy using these values:
will try all the combinations with all different values in the search
for best working algo.
```
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if 'adx-enabled' in params and params['adx-enabled']:
conditions.append(dataframe['adx'] > params['adx-value'])
if 'rsi-enabled' in params and params['rsi-enabled']:
conditions.append(dataframe['rsi'] < params['rsi-value'])
### Add a new Indicators # TRIGGERS
If you want to test an indicator that isn't used by the bot currently, if params['trigger'] == 'bb_lower':
you need to add it to the `populate_indicators()` method in `hyperopt.py`. conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['trigger'] == 'macd_cross_signal':
conditions.append(qtpylib.crossed_above(
dataframe['macd'], dataframe['macdsignal']
))
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
return dataframe
return populate_buy_trend
```
Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`)
with different value combinations. It will then use the given historical data and make
buys based on the buy signals generated with the above function and based on the results
it will end with telling you which paramter combination produced the best profits.
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
that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`.
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
add it to the `populate_indicators()` method in `hyperopt.py`.
## 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.
@@ -164,12 +125,12 @@ python3 ./freqtrade/main.py -c config.json hyperopt -e 5000
The `-e` flag will set how many evaluations hyperopt will do. We recommend The `-e` flag will set how many evaluations hyperopt will do. We recommend
running at least several thousand evaluations. running at least several thousand evaluations.
### Execute hyperopt with different ticker-data source ### Execute Hyperopt with Different Ticker-Data Source
If you would like to hyperopt parameters using an alternate ticker data that If you would like to hyperopt parameters using an alternate ticker data that
you have on-disk, use the `--datadir PATH` option. Default hyperopt will you have on-disk, use the `--datadir PATH` option. Default hyperopt will
use data from directory `user_data/data`. use data from directory `user_data/data`.
### Running hyperopt with smaller testset ### Running Hyperopt with Smaller Testset
Use the `--timeperiod` argument to change how much of the testset Use the `--timeperiod` argument to change how much of the testset
you want to use. The last N ticks/timeframes will be used. you want to use. The last N ticks/timeframes will be used.
Example: Example:
@@ -178,7 +139,7 @@ Example:
python3 ./freqtrade/main.py hyperopt --timeperiod -200 python3 ./freqtrade/main.py hyperopt --timeperiod -200
``` ```
### Running hyperopt with smaller search space ### Running Hyperopt with Smaller Search Space
Use the `--spaces` argument to limit the search space used by hyperopt. Use the `--spaces` argument to limit the search space used by hyperopt.
Letting Hyperopt optimize everything is a huuuuge search space. Often it Letting Hyperopt optimize everything is a huuuuge search space. Often it
might make more sense to start by just searching for initial buy algorithm. might make more sense to start by just searching for initial buy algorithm.
@@ -193,87 +154,44 @@ Legal values are:
- `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`
## Understand the hyperopts result ## Understand the Hyperopts Result
Once Hyperopt is completed you can use the result to adding new buy Once Hyperopt is completed you can use the result to create a new strategy.
signal. Given following result from hyperopt: Given the following result from hyperopt:
```
Best parameters:
{
"adx": {
"enabled": true,
"value": 15.0
},
"fastd": {
"enabled": true,
"value": 40.0
},
"green_candle": {
"enabled": true
},
"mfi": {
"enabled": false
},
"over_sar": {
"enabled": false
},
"rsi": {
"enabled": true,
"value": 37.0
},
"trigger": {
"type": "lower_bb"
},
"uptrend_long_ema": {
"enabled": true
},
"uptrend_short_ema": {
"enabled": false
},
"uptrend_sma": {
"enabled": false
}
}
Best Result: ```
2197 trades. Avg profit 1.84%. Total profit 0.79367541 BTC. Avg duration 241.0 mins. Best result:
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
with values:
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower'}
``` ```
You should understand this result like: You should understand this result like:
- You should **consider** the guard "adx" (`"adx"` is `"enabled": true`) - The buy trigger that worked best was `bb_lower`.
and the best value is `15.0` (`"value": 15.0,`) - You should not use ADX because `adx-enabled: False`)
- You should **consider** the guard "fastd" (`"fastd"` is `"enabled": - You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
true`) and the best value is `40.0` (`"value": 40.0,`)
- You should **consider** to enable the guard "green_candle"
(`"green_candle"` is `"enabled": true`) but this guards as no
customizable value.
- You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`)
- and so on...
You have to look inside your strategy file into `buy_strategy_generator()` You have to look inside your strategy file into `buy_strategy_generator()`
method, what those values match to. method, what those values match to.
So for example you had `adx:` with the `value: 15.0` so we would look So for example you had `rsi-value: 29.0` so we would look
at `adx`-block, that translates to the following code block: at `rsi`-block, that translates to the following code block:
``` ```
(dataframe['adx'] > 15.0) (dataframe['rsi'] < 29.0)
``` ```
Translating your whole hyperopt result to as the new buy-signal Translating your whole hyperopt result as the new buy-signal
would be the following: would then look like:
``` ```
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 15.0) & # adx-value (dataframe['rsi'] < 29.0) & # rsi-value
(dataframe['fastd'] < 40.0) & # fastd-value dataframe['close'] < dataframe['bb_lowerband'] # trigger
(dataframe['close'] > dataframe['open']) & # green_candle
(dataframe['rsi'] < 37.0) & # rsi-value
(dataframe['ema50'] > dataframe['ema100']) # uptrend_long_ema
), ),
'buy'] = 1 'buy'] = 1
return dataframe return dataframe
``` ```
## Next step ## Next Step
Now you have a perfect bot and want to control it from Telegram. Your Now you have a perfect bot and want to control it from Telegram. Your
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md). next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).

View File

@@ -1,4 +1,5 @@
# freqtrade documentation # freqtrade documentation
Welcome to freqtrade documentation. Please feel free to contribute to Welcome to freqtrade documentation. Please feel free to contribute to
this documentation if you see it became outdated by sending us a this documentation if you see it became outdated by sending us a
Pull-request. Do not hesitate to reach us on Pull-request. Do not hesitate to reach us on
@@ -6,6 +7,7 @@ Pull-request. Do not hesitate to reach us on
if you do not find the answer to your questions. if you do not find the answer to your questions.
## Table of Contents ## Table of Contents
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md) - [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account) - [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot) - [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
@@ -25,8 +27,10 @@ Pull-request. Do not hesitate to reach us on
- [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) - [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md) - [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md)
- [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md)
- [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) - [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
- [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) - [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
- [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) - [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
- [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md) - [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md)
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md) - [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)
- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md))

View File

@@ -8,7 +8,6 @@ To understand how to set up the bot please read the [Bot Configuration](https://
* [Table of Contents](#table-of-contents) * [Table of Contents](#table-of-contents)
* [Easy Installation - Linux Script](#easy-installation---linux-script) * [Easy Installation - Linux Script](#easy-installation---linux-script)
* [Manual installation](#manual-installation)
* [Automatic Installation - Docker](#automatic-installation---docker) * [Automatic Installation - Docker](#automatic-installation---docker)
* [Custom Linux MacOS Installation](#custom-installation) * [Custom Linux MacOS Installation](#custom-installation)
- [Requirements](#requirements) - [Requirements](#requirements)
@@ -56,28 +55,6 @@ Reset parameter will hard reset your branch (only if you are on `master` or `dev
Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`.
## Manual installation - Linux/MacOS
The following steps are made for Linux/MacOS environment
**1. Clone the repo**
```bash
git clone git@github.com:freqtrade/freqtrade.git
git checkout develop
cd freqtrade
```
**2. Create the config file**
Switch `"dry_run": true,`
```bash
cp config.json.example config.json
vi config.json
```
**3. Build your docker image and run it**
```bash
docker build -t freqtrade .
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
------ ------
## Automatic Installation - Docker ## Automatic Installation - Docker
@@ -190,7 +167,7 @@ docker run -d \
freqtrade --db-url sqlite:///tradesv3.sqlite freqtrade --db-url sqlite:///tradesv3.sqlite
``` ```
NOTE: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. *Note*: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
### 6. Monitor your Docker instance ### 6. Monitor your Docker instance
@@ -205,14 +182,15 @@ docker stop freqtrade
docker start freqtrade docker start freqtrade
``` ```
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/).
*Note*: You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
### 7. Backtest with docker ### 7. Backtest with docker
The following assumes that the above steps (1-4) have been completed successfully. The following assumes that the above steps (1-4) have been completed successfully.
Also, backtest-data should be available at `~/.freqtrade/user_data/`. Also, backtest-data should be available at `~/.freqtrade/user_data/`.
``` bash ``` bash
docker run -d \ docker run -d \
--name freqtrade \ --name freqtrade \
@@ -232,12 +210,13 @@ Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtr
## Custom Installation ## Custom Installation
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.
### Requirements ### Requirements
Click each one for install guide: Click each one for install guide:
* [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/), note the bot was not tested on Python >= 3.7.x * [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
* [pip](https://pip.pypa.io/en/stable/installing/) * [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) * [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
@@ -245,7 +224,7 @@ Click each one for install guide:
### Linux - Ubuntu 16.04 ### Linux - Ubuntu 16.04
#### 1. Install Python 3.6, Git, and wget #### Install Python 3.6, Git, and wget
```bash ```bash
sudo add-apt-repository ppa:jonathonf/python-3.6 sudo add-apt-repository ppa:jonathonf/python-3.6
@@ -253,7 +232,34 @@ 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 python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
``` ```
#### 2. Install TA-Lib #### Raspberry Pi / Raspbian
Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/).
The following assumes that miniconda3 is installed and available in your environment, and is installed.
It's recommended to use (mini)conda for this as installation/compilation of `scipy` and `pandas` takes a long time.
``` bash
conda config --add channels rpi
conda install python=3.6
conda create -n freqtrade python=3.6
conda install scipy pandas
pip install -r requirements.txt
pip install -e .
```
### MacOS
#### Install Python 3.6, git, wget and ta-lib
```bash
brew install python3 git wget
```
### common
#### 1. Install TA-Lib
Official webpage: https://mrjbq7.github.io/ta-lib/install.html Official webpage: https://mrjbq7.github.io/ta-lib/install.html
@@ -261,6 +267,7 @@ Official webpage: https://mrjbq7.github.io/ta-lib/install.html
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
tar xvzf ta-lib-0.4.0-src.tar.gz tar xvzf 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
./configure --prefix=/usr ./configure --prefix=/usr
make make
make install make install
@@ -268,15 +275,60 @@ cd ..
rm -rf ./ta-lib* rm -rf ./ta-lib*
``` ```
*Note*: An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently.
#### 2. Setup your Python virtual environment (virtualenv)
*Note*: This step is optional but strongly recommended to keep your system organized
```bash
python3 -m venv .env
source .env/bin/activate
```
#### 3. Install FreqTrade #### 3. Install FreqTrade
Clone the git repository: Clone the git repository:
```bash ```bash
git clone https://github.com/freqtrade/freqtrade.git git clone https://github.com/freqtrade/freqtrade.git
``` ```
#### 4. Configure `freqtrade` as a `systemd` service Optionally checkout the stable/master branch:
```bash
git checkout master
```
#### 4. Initialize the configuration
```bash
cd freqtrade
cp config.json.example config.json
```
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
#### 5. Install python dependencies
``` bash
pip3 install --upgrade pip
pip3 install -r requirements.txt
pip3 install -e .
```
#### 6. Run the Bot
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
python3.6 ./freqtrade/main.py -c config.json
```
*Note*: If you run the bot on a server, you should consider using [Docker](#automatic-installation---docker) a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
#### 7. [Optional] Configure `freqtrade` as a `systemd` service
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup. From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
@@ -292,57 +344,6 @@ For this to be persistent (run when user is logged out) you'll need to enable `l
sudo loginctl enable-linger "$USER" sudo loginctl enable-linger "$USER"
``` ```
### MacOS
#### 1. Install Python 3.6, git, wget and ta-lib
```bash
brew install python3 git wget ta-lib
```
#### 2. Install FreqTrade
Clone the git repository:
```bash
git clone https://github.com/freqtrade/freqtrade.git
```
Optionally checkout the develop branch:
```bash
git checkout develop
```
### Setup Config and virtual env
#### 1. Initialize the configuration
```bash
cd freqtrade
cp config.json.example config.json
```
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
#### 2. Setup your Python virtual environment (virtualenv)
```bash
python3.6 -m venv .env
source .env/bin/activate
pip3.6 install --upgrade pip
pip3.6 install -r requirements.txt
pip3.6 install -e .
```
#### 3. Run the Bot
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
python3.6 ./freqtrade/main.py -c config.json
```
------ ------
## Windows ## Windows
@@ -362,7 +363,7 @@ git clone https://github.com/freqtrade/freqtrade.git
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade` 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).
@@ -383,5 +384,17 @@ REM >pip install TA_Lib0.4.17cp36cp36mwin32.whl
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) > Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
#### Error during installation under Windows
``` bash
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
```
Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or docker first.
---
Now you have an environment ready, the next step is Now you have an environment ready, the next step is
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)... [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...

View File

@@ -24,7 +24,7 @@ script/plot_dataframe.py [-h] [-p pair] [--live]
Example Example
``` ```
python scripts/plot_dataframe.py -p BTC_ETH python scripts/plot_dataframe.py -p BTC/ETH
``` ```
The `-p` pair argument, can be used to specify what The `-p` pair argument, can be used to specify what
@@ -34,18 +34,18 @@ pair you would like to plot.
To plot the current live price use the `--live` flag: To plot the current live price use the `--live` flag:
``` ```
python scripts/plot_dataframe.py -p BTC_ETH --live python scripts/plot_dataframe.py -p BTC/ETH --live
``` ```
To plot a timerange (to zoom in): To plot a timerange (to zoom in):
``` ```
python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200 python scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200
``` ```
Timerange doesn't work with live data. 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:
``` ```
python scripts/plot_dataframe.py --db-url tradesv3.dry_run.sqlite -p BTC_ETH python scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH
``` ```
To plot a test strategy the strategy should have first be backtested. To plot a test strategy the strategy should have first be backtested.

141
docs/sandbox-testing.md Normal file
View File

@@ -0,0 +1,141 @@
# Sandbox API testing
Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these.
This document is a *light overview of configuring Freqtrade and GDAX sandbox.
This can be useful to developers and trader alike as Freqtrade is quite customisable.
When testing your API connectivity, make sure to use the following URLs.
***Website**
https://public.sandbox.gdax.com
***REST API**
https://api-public.sandbox.gdax.com
---
# Configure a Sandbox account on Gdax
Aim of this document section
- An sanbox account
- create 2FA (needed to create an API)
- Add test 50BTC to account
- Create :
- - API-KEY
- - API-Secret
- - API Password
## Acccount
This link will redirect to the sandbox main page to login / create account dialogues:
https://public.sandbox.pro.coinbase.com/orders/
After registration and Email confimation you wil be redirected into your sanbox account. It is easy to verify you're in sandbox by checking the URL bar.
> https://public.sandbox.pro.coinbase.com/
## Enable 2Fa (a prerequisite to creating sandbox API Keys)
From within sand box site select your profile, top right.
>Or as a direct link: https://public.sandbox.pro.coinbase.com/profile
From the menu panel to the left of the screen select
> Security: "*View or Update*"
In the new site select "enable authenticator" as typical google Authenticator.
- open Google Authenticator on your phone
- scan barcode
- enter your generated 2fa
## Enable API Access
From within sandbox select profile>api>create api-keys
>or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api
Click on "create one" and ensure **view** and **trade** are "checked" and sumbit your 2FA
- **Copy and paste the Passphase** into a notepade this will be needed later
- **Copy and paste the API Secret** popup into a notepad this will needed later
- **Copy and paste the API Key** into a notepad this will needed later
## Add 50 BTC test funds
To add funds, use the web interface deposit and withdraw buttons.
To begin select 'Wallets' from the top menu.
> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets
- Deposits (bottom left of screen)
- - Deposit Funds Bitcoin
- - - Coinbase BTC Wallet
- - - - Max (50 BTC)
- - - - - Deposit
*This process may be repeated for other currencies, ETH as example*
---
# Configure Freqtrade to use Gax Sandbox
The aim of this document section
- Enable sandbox URLs in Freqtrade
- Configure API
- - secret
- - key
- - passphrase
## Sandbox URLs
Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade.
These include `['test']` and `['api']`.
- `[Test]` if available will point to an Exchanges sandbox.
- `[Api]` normally used, and resolves to live API target on the exchange
To make use of sandbox / test add "sandbox": true, to your config.json
```json
"exchange": {
"name": "gdax",
"sandbox": true,
"key": "5wowfxemogxeowo;heiohgmd",
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
"password": "1bkjfkhfhfu6sr",
"outdated_offset": 5
"pair_whitelist": [
"BTC/USD"
```
Also insert your
- api-key (noted earlier)
- api-secret (noted earlier)
- password (the passphrase - noted earlier)
---
## You should now be ready to test your sandbox
Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox.
** Typically the BTC/USD has the most activity in sandbox to test against.
## GDAX - Old Candles problem
It is my experience that GDAX sandbox candles may be 20+- minutes out of date. This can cause trades to fail as one of Freqtrades safety checks.
To disable this check, add / change the `"outdated_offset"` parameter in the exchange section of your configuration to adjust for this delay.
Example based on the above configuration:
```json
"exchange": {
"name": "gdax",
"sandbox": true,
"key": "5wowfxemogxeowo;heiohgmd",
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
"password": "1bkjfkhfhfu6sr",
"outdated_offset": 30
"pair_whitelist": [
"BTC/USD"
```

View File

@@ -59,7 +59,7 @@ SELECT * FROM trades;
```sql ```sql
UPDATE trades UPDATE trades
SET is_open=0, close_date=<close_date>, close_rate=<close_rate>, close_profit=close_rate/open_rate SET is_open=0, close_date=<close_date>, close_rate=<close_rate>, close_profit=close_rate/open_rate-1
WHERE id=<trade_ID_to_update>; WHERE id=<trade_ID_to_update>;
``` ```

51
docs/stoploss.md Normal file
View File

@@ -0,0 +1,51 @@
# Stop Loss support
At this stage the bot contains the following stoploss support modes:
1. static stop loss, defined in either the strategy or configuration
2. trailing stop loss, defined in the configuration
3. trailing stop loss, custom positive loss, defined in configuration
## Static Stop Loss
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
## Trail Stop Loss
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally.
To enable this Feauture all you have to do is to define the configuration element:
``` json
"trailing_stop" : True
```
This will now activate an algorithm, which automatically moves your stop loss up every time the price of your asset increases.
For example, simplified math,
* you buy an asset at a price of 100$
* your stop loss is defined at 2%
* which means your stop loss, gets triggered once your asset dropped below 98$
* assuming your asset now increases to 102$
* your stop loss, will now be 2% of 102$ or 99.96$
* now your asset drops in value to 101$, your stop loss, will still be 99.96$
basically what this means is that your stop loss will be adjusted to be always be 2% of the highest observed price
### Custom positive loss
Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage,
the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you have 1.1% profit,
it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them.
Both values can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true.
``` json
"trailing_stop_positive": 0.01,
"trailing_stop_positive_offset": 0.011,
```
The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit.
You should also make sure to have this value higher than your minimal ROI, otherwise minimal ROI will apply first and sell your trade.

74
docs/webhook-config.md Normal file
View File

@@ -0,0 +1,74 @@
# Webhook usage
This page explains how to configure your bot to talk to webhooks.
## Configuration
Enable webhooks by adding a webhook-section to your configuration file, and setting `webhook.enabled` to `true`.
Sample configuration (tested using IFTTT).
```json
"webhook": {
"enabled": true,
"url": "https://maker.ifttt.com/trigger/<YOUREVENT>/with/key/<YOURKEY>/",
"webhookbuy": {
"value1": "Buying {pair}",
"value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}"
},
"webhooksell": {
"value1": "Selling {pair}",
"value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency}"
},
"webhookstatus": {
"value1": "Status: {status}",
"value2": "",
"value3": ""
}
},
```
The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url.
Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called.
### Webhookbuy
The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format.
Possible parameters are:
* exchange
* pair
* market_url
* limit
* stake_amount
* stake_amount_fiat
* stake_currency
* fiat_currency
### Webhooksell
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
Possible parameters are:
* exchange
* pair
* gain
* market_url
* limit
* amount
* open_rate
* current_rate
* profit_amount
* profit_percent
* profit_fiat
* stake_currency
* fiat_currency
### Webhookstatus
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
The only possible value here is `{status}`.

View File

@@ -1,5 +1,5 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '0.17.0' __version__ = '0.17.1'
class DependencyException(BaseException): class DependencyException(BaseException):

View File

@@ -7,8 +7,8 @@ To launch Freqtrade as a module
""" """
import sys import sys
from freqtrade import main
from freqtrade import main
if __name__ == '__main__': if __name__ == '__main__':
main.set_loggers() main.set_loggers()

View File

@@ -1,237 +0,0 @@
"""
Functions to analyze ticker data with indicators and produce buy and sell signals
"""
import logging
from datetime import datetime, timedelta
from enum import Enum
from typing import Dict, List, Tuple
import arrow
from pandas import DataFrame, to_datetime
from freqtrade import constants
from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
logger = logging.getLogger(__name__)
class SignalType(Enum):
"""
Enum to distinguish between buy and sell signals
"""
BUY = "buy"
SELL = "sell"
class Analyze(object):
"""
Analyze class contains everything the bot need to determine if the situation is good for
buying or selling.
"""
def __init__(self, config: dict) -> None:
"""
Init Analyze
:param config: Bot configuration (use the one from Configuration())
"""
self.config = config
self.strategy: IStrategy = StrategyResolver(self.config).strategy
@staticmethod
def parse_ticker_dataframe(ticker: list) -> DataFrame:
"""
Analyses the trend for the given ticker history
:param ticker: See exchange.get_ticker_history
:return: DataFrame
"""
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
frame = DataFrame(ticker, columns=cols)
frame['date'] = to_datetime(frame['date'],
unit='ms',
utc=True,
infer_datetime_format=True)
# group by index and aggregate results to eliminate duplicate ticks
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'max',
})
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
return frame
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
"""
return self.strategy.populate_indicators(dataframe=dataframe)
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
return self.strategy.populate_buy_trend(dataframe=dataframe)
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
return self.strategy.populate_sell_trend(dataframe=dataframe)
def get_ticker_interval(self) -> str:
"""
Return ticker interval to use
:return: Ticker interval value to use
"""
return self.strategy.ticker_interval
def get_stoploss(self) -> float:
"""
Return stoploss to use
:return: Strategy stoploss value to use
"""
return self.strategy.stoploss
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
"""
Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
"""
dataframe = self.parse_ticker_dataframe(ticker_history)
dataframe = self.populate_indicators(dataframe)
dataframe = self.populate_buy_trend(dataframe)
dataframe = self.populate_sell_trend(dataframe)
return dataframe
def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]:
"""
Calculates current signal based several technical analysis indicators
:param pair: pair in format ANT/BTC
:param interval: Interval to use (in min)
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
"""
ticker_hist = exchange.get_ticker_history(pair, interval)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return False, False
try:
dataframe = self.analyze_ticker(ticker_hist)
except ValueError as error:
logger.warning(
'Unable to analyze ticker for pair %s: %s',
pair,
str(error)
)
return False, False
except Exception as error:
logger.exception(
'Unexpected error when analyzing ticker for pair %s: %s',
pair,
str(error)
)
return False, False
if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair)
return False, False
latest = dataframe.iloc[-1]
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
if signal_date < (arrow.utcnow() - timedelta(minutes=(interval_minutes + 5))):
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,
(arrow.utcnow() - signal_date).seconds // 60
)
return False, False
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
logger.debug(
'trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'],
pair,
str(buy),
str(sell)
)
return buy, sell
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
"""
This function evaluate if on the condition required to trigger a sell has been reached
if the threshold is reached and updates the trade record.
:return: True if trade should be sold, False otherwise
"""
current_profit = trade.calc_profit_percent(rate)
if self.stop_loss_reached(current_profit=current_profit):
return True
experimental = self.config.get('experimental', {})
if buy and experimental.get('ignore_roi_if_buy_signal', False):
logger.debug('Buy signal still active - not selling.')
return False
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
logger.debug('Required profit reached. Selling..')
return True
if experimental.get('sell_profit_only', False):
logger.debug('Checking if trade is profitable..')
if trade.calc_profit(rate=rate) <= 0:
return False
if sell and not buy and experimental.get('use_sell_signal', False):
logger.debug('Sell signal received. Selling..')
return True
return False
def stop_loss_reached(self, current_profit: float) -> bool:
"""Based on current profit of the trade and configured stoploss, decides to sell or not"""
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
logger.debug('Stop loss hit.')
return True
return False
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
sell
:return True if bot should sell at current rate
"""
# Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
for duration, threshold in self.strategy.minimal_roi.items():
if time_diff <= duration:
return False
if current_profit > threshold:
return True
return False
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""
Creates a dataframe and populates indicators for given ticker data
"""
return {pair: self.populate_indicators(self.parse_ticker_dataframe(pair_data))
for pair, pair_data in tickerdata.items()}

View File

@@ -2,12 +2,12 @@
This module contains the argument manager class This module contains the argument manager class
""" """
import os
import argparse import argparse
import logging import os
import re import re
from typing import List, NamedTuple, Optional
import arrow import arrow
from typing import List, Optional, NamedTuple
from freqtrade import __version__, constants from freqtrade import __version__, constants
@@ -63,11 +63,10 @@ class Arguments(object):
""" """
self.parser.add_argument( self.parser.add_argument(
'-v', '--verbose', '-v', '--verbose',
help='be verbose', help='verbose mode (-vv for more, -vvv to get all messages)',
action='store_const', action='count',
dest='loglevel', dest='loglevel',
const=logging.DEBUG, default=0,
default=logging.INFO,
) )
self.parser.add_argument( self.parser.add_argument(
'--version', '--version',
@@ -120,7 +119,6 @@ class Arguments(object):
help='Override trades database URL, this is useful if dry_run is enabled' help='Override trades database URL, this is useful if dry_run is enabled'
' or in custom deployments (default: %(default)s)', ' or in custom deployments (default: %(default)s)',
dest='db_url', dest='db_url',
default=constants.DEFAULT_DB_PROD_URL,
type=str, type=str,
metavar='PATH', metavar='PATH',
) )
@@ -143,6 +141,16 @@ class Arguments(object):
action='store_true', action='store_true',
dest='refresh_pairs', dest='refresh_pairs',
) )
parser.add_argument(
'--strategy-list',
help='Provide a commaseparated 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( parser.add_argument(
'--export', '--export',
help='export backtest results, argument are: trades\ help='export backtest results, argument are: trades\
@@ -177,11 +185,22 @@ class Arguments(object):
type=str, type=str,
) )
parser.add_argument( parser.add_argument(
'--realistic-simulation', '--eps', '--enable-position-stacking',
help='uses max_open_trades from config to simulate real world limitations', help='Allow buying the same pair multiple times (position stacking)',
action='store_true', action='store_true',
dest='realistic_simulation', 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( parser.add_argument(
'--timerange', '--timerange',
help='specify what timerange of data to use.', help='specify what timerange of data to use.',
@@ -334,3 +353,10 @@ class Arguments(object):
nargs='+', nargs='+',
dest='timeframes', dest='timeframes',
) )
self.parser.add_argument(
'--erase',
help='Clean all existing data for the selected exchange/pairs/timeframes',
dest='erase',
action='store_true'
)

View File

@@ -1,21 +1,33 @@
""" """
This module contains the configuration class This module contains the configuration class
""" """
import os
import json import json
import logging import logging
import os
from argparse import Namespace from argparse import Namespace
from typing import Optional, Dict, Any from typing import Any, Dict, Optional
import ccxt
from jsonschema import Draft4Validator, validate from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match from jsonschema.exceptions import ValidationError, best_match
import ccxt
from freqtrade import OperationalException, constants from freqtrade import OperationalException, constants
logger = logging.getLogger(__name__) 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)
class Configuration(object): class Configuration(object):
""" """
Class to read and init the bot configuration Class to read and init the bot configuration
@@ -62,8 +74,8 @@ class Configuration(object):
conf = json.load(file) conf = json.load(file)
except FileNotFoundError: except FileNotFoundError:
raise OperationalException( raise OperationalException(
'Config file "{}" not found!' f'Config file "{path}" not found!'
' Please create a config file or check whether it exists.'.format(path)) ' Please create a config file or check whether it exists.')
if 'internals' not in conf: if 'internals' not in conf:
conf['internals'] = {} conf['internals'] = {}
@@ -79,12 +91,15 @@ class Configuration(object):
# Log level # Log level
if 'loglevel' in self.args and self.args.loglevel: if 'loglevel' in self.args and self.args.loglevel:
config.update({'loglevel': self.args.loglevel}) config.update({'verbosity': self.args.loglevel})
logging.basicConfig( else:
level=config['loglevel'], config.update({'verbosity': 0})
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', logging.basicConfig(
) level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG,
logger.info('Log level set to %s', logging.getLevelName(config['loglevel'])) format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
set_loggers(config['verbosity'])
logger.info('Verbosity set to %s', config['verbosity'])
# Add dynamic_whitelist if found # Add dynamic_whitelist if found
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
@@ -95,9 +110,12 @@ class Configuration(object):
'(not applicable with Backtesting and Hyperopt)' '(not applicable with Backtesting and Hyperopt)'
) )
if self.args.db_url != constants.DEFAULT_DB_PROD_URL: if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL:
config.update({'db_url': self.args.db_url}) config.update({'db_url': self.args.db_url})
logger.info('Parameter --db-url detected ...') logger.info('Parameter --db-url detected ...')
else:
# Set default here
config.update({'db_url': constants.DEFAULT_DB_PROD_URL})
if config.get('dry_run', False): if config.get('dry_run', False):
logger.info('Dry run is enabled') logger.info('Dry run is enabled')
@@ -109,7 +127,7 @@ class Configuration(object):
config['db_url'] = constants.DEFAULT_DB_PROD_URL config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled') logger.info('Dry run is disabled')
logger.info('Using DB: "{}"'.format(config['db_url'])) logger.info(f'Using DB: "{config["db_url"]}"')
# Check if the exchange set by the user is supported # Check if the exchange set by the user is supported
self.check_exchange(config) self.check_exchange(config)
@@ -142,11 +160,18 @@ class Configuration(object):
config.update({'live': True}) config.update({'live': True})
logger.info('Parameter -l/--live detected ...') logger.info('Parameter -l/--live detected ...')
# If --realistic-simulation is used we add it to the configuration # If --enable-position-stacking is used we add it to the configuration
if 'realistic_simulation' in self.args and self.args.realistic_simulation: if 'position_stacking' in self.args and self.args.position_stacking:
config.update({'realistic_simulation': True}) config.update({'position_stacking': True})
logger.info('Parameter --realistic-simulation detected ...') logger.info('Parameter --enable-position-stacking detected ...')
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
# If --disable-max-market-positions is used we add it to the configuration
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 ...')
else:
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
# If --timerange is used we add it to the configuration # If --timerange is used we add it to the configuration
if 'timerange' in self.args and self.args.timerange: if 'timerange' in self.args and self.args.timerange:
@@ -165,6 +190,14 @@ class Configuration(object):
config.update({'refresh_pairs': True}) config.update({'refresh_pairs': True})
logger.info('Parameter -r/--refresh-pairs-cached detected ...') logger.info('Parameter -r/--refresh-pairs-cached detected ...')
if 'strategy_list' in self.args and self.args.strategy_list:
config.update({'strategy_list': self.args.strategy_list})
logger.info('Using strategy list of %s Strategies', len(self.args.strategy_list))
if 'ticker_interval' in self.args and self.args.ticker_interval:
config.update({'ticker_interval': self.args.ticker_interval})
logger.info('Overriding ticker interval with Command line argument')
# If --export is used we add it to the configuration # If --export is used we add it to the configuration
if 'export' in self.args and self.args.export: if 'export' in self.args and self.args.export:
config.update({'export': self.args.export}) config.update({'export': self.args.export})
@@ -182,7 +215,7 @@ class Configuration(object):
Extract information for sys.argv and load Hyperopt configuration Extract information for sys.argv and load Hyperopt configuration
:return: configuration as dictionary :return: configuration as dictionary
""" """
# If --realistic-simulation is used we add it to the configuration # If --epochs is used we add it to the configuration
if 'epochs' in self.args and self.args.epochs: if 'epochs' in self.args and self.args.epochs:
config.update({'epochs': self.args.epochs}) config.update({'epochs': self.args.epochs})
logger.info('Parameter --epochs detected ...') logger.info('Parameter --epochs detected ...')

View File

@@ -36,7 +36,7 @@ SUPPORTED_FIAT = [
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT" "BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT"
] ]
# Required json-schema for user specified config # Required json-schema for user specified config
@@ -45,7 +45,7 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 0}, 'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())}, 'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': { 'stake_amount': {
"type": ["number", "string"], "type": ["number", "string"],
"minimum": 0.0005, "minimum": 0.0005,
@@ -53,6 +53,7 @@ CONF_SCHEMA = {
}, },
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'}, 'dry_run': {'type': 'boolean'},
'process_only_new_candles': {'type': 'boolean'},
'minimal_roi': { 'minimal_roi': {
'type': 'object', 'type': 'object',
'patternProperties': { 'patternProperties': {
@@ -61,7 +62,16 @@ CONF_SCHEMA = {
'minProperties': 1 'minProperties': 1
}, },
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'unfilledtimeout': {'type': 'integer', 'minimum': 0}, 'trailing_stop': {'type': 'boolean'},
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1},
'unfilledtimeout': {
'type': 'object',
'properties': {
'buy': {'type': 'number', 'minimum': 3},
'sell': {'type': 'number', 'minimum': 10}
}
},
'bid_strategy': { 'bid_strategy': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
@@ -69,18 +79,35 @@ CONF_SCHEMA = {
'type': 'number', 'type': 'number',
'minimum': 0, 'minimum': 0,
'maximum': 1, 'maximum': 1,
'exclusiveMaximum': False 'exclusiveMaximum': False,
'use_order_book': {'type': 'boolean'},
'order_book_top': {'type': 'number', 'maximum': 20, 'minimum': 1},
'check_depth_of_market': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'bids_to_ask_delta': {'type': 'number', 'minimum': 0},
}
},
}, },
}, },
'required': ['ask_last_balance'] 'required': ['ask_last_balance']
}, },
'ask_strategy': {
'type': 'object',
'properties': {
'use_order_book': {'type': 'boolean'},
'order_book_min': {'type': 'number', 'minimum': 1},
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}
}
},
'exchange': {'$ref': '#/definitions/exchange'}, 'exchange': {'$ref': '#/definitions/exchange'},
'experimental': { 'experimental': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'use_sell_signal': {'type': 'boolean'}, 'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'},
"ignore_roi_if_buy_signal_true": {'type': 'boolean'} 'ignore_roi_if_buy_signal_true': {'type': 'boolean'}
} }
}, },
'telegram': { 'telegram': {
@@ -92,6 +119,15 @@ CONF_SCHEMA = {
}, },
'required': ['enabled', 'token', 'chat_id'] 'required': ['enabled', 'token', 'chat_id']
}, },
'webhook': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'webhookbuy': {'type': 'object'},
'webhooksell': {'type': 'object'},
'webhookstatus': {'type': 'object'},
},
},
'db_url': {'type': 'string'}, 'db_url': {'type': 'string'},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': { 'internals': {
@@ -107,8 +143,11 @@ CONF_SCHEMA = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'name': {'type': 'string'}, 'name': {'type': 'string'},
'sandbox': {'type': 'boolean'},
'key': {'type': 'string'}, 'key': {'type': 'string'},
'secret': {'type': 'string'}, 'secret': {'type': 'string'},
'password': {'type': 'string'},
'uid': {'type': 'string'},
'pair_whitelist': { 'pair_whitelist': {
'type': 'array', 'type': 'array',
'items': { 'items': {
@@ -124,7 +163,8 @@ CONF_SCHEMA = {
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
}, },
'uniqueItems': True 'uniqueItems': True
} },
'outdated_offset': {'type': 'integer', 'minimum': 1}
}, },
'required': ['name', 'key', 'secret', 'pair_whitelist'] 'required': ['name', 'key', 'secret', 'pair_whitelist']
} }
@@ -136,7 +176,6 @@ CONF_SCHEMA = {
'max_open_trades', 'max_open_trades',
'stake_currency', 'stake_currency',
'stake_amount', 'stake_amount',
'fiat_display_currency',
'dry_run', 'dry_run',
'bid_strategy', 'bid_strategy',
'telegram' 'telegram'

View File

@@ -1,11 +1,15 @@
# pragma pylint: disable=W0603 # pragma pylint: disable=W0603
""" Cryptocurrency Exchanges support """ """ Cryptocurrency Exchanges support """
import logging import logging
import inspect
from random import randint from random import randint
from typing import List, Dict, Any, Optional from typing import List, Dict, Tuple, Any, Optional
from datetime import datetime from datetime import datetime
from math import floor, ceil
import asyncio
import ccxt import ccxt
import ccxt.async_support as ccxt_async
import arrow import arrow
from freqtrade import constants, OperationalException, DependencyException, TemporaryError from freqtrade import constants, OperationalException, DependencyException, TemporaryError
@@ -22,6 +26,24 @@ _EXCHANGE_URLS = {
} }
def retrier_async(f):
async def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT)
try:
return await f(*args, **kwargs)
except (TemporaryError, DependencyException) as ex:
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0:
count -= 1
kwargs.update({'count': count})
logger.warning('retrying %s() still for %s times', f.__name__, count)
return await wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper
def retrier(f): def retrier(f):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT) count = kwargs.pop('count', API_RETRY_COUNT)
@@ -44,8 +66,8 @@ class Exchange(object):
# Current selected exchange # Current selected exchange
_api: ccxt.Exchange = None _api: ccxt.Exchange = None
_api_async: ccxt_async.Exchange = None
_conf: Dict = {} _conf: Dict = {}
_cached_ticker: Dict[str, Any] = {}
# Holds all open sell orders for dry_run # Holds all open sell orders for dry_run
_dry_run_open_orders: Dict[str, Any] = {} _dry_run_open_orders: Dict[str, Any] = {}
@@ -59,18 +81,40 @@ class Exchange(object):
""" """
self._conf.update(config) self._conf.update(config)
self._cached_ticker: Dict[str, Any] = {}
# Holds last candle refreshed time of each pair
self._pairs_last_refresh_time: Dict[str, int] = {}
# Holds candles
self.klines: Dict[str, Any] = {}
if config['dry_run']: if config['dry_run']:
logger.info('Instance is running with dry_run enabled') logger.info('Instance is running with dry_run enabled')
exchange_config = config['exchange'] exchange_config = config['exchange']
self._api = self._init_ccxt(exchange_config) self._api = self._init_ccxt(exchange_config)
self._api_async = self._init_ccxt(exchange_config, ccxt_async)
logger.info('Using Exchange "%s"', self.name) logger.info('Using Exchange "%s"', self.name)
self.markets = self._load_markets()
# Check if all pairs are available # Check if all pairs are available
self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_pairs(config['exchange']['pair_whitelist'])
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange: if config.get('ticker_interval'):
# Check if timeframe is available
self.validate_timeframes(config['ticker_interval'])
def __del__(self):
"""
Destructor - clean up async stuff
"""
logger.debug("Exchange object destroyed, closing async loop")
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
asyncio.get_event_loop().run_until_complete(self._api_async.close())
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt) -> ccxt.Exchange:
""" """
Initialize ccxt with given config and return valid Initialize ccxt with given config and return valid
ccxt instance. ccxt instance.
@@ -78,19 +122,21 @@ class Exchange(object):
# Find matching class for the given exchange name # Find matching class for the given exchange name
name = exchange_config['name'] name = exchange_config['name']
if name not in ccxt.exchanges: if name not in ccxt_module.exchanges:
raise OperationalException(f'Exchange {name} is not supported') raise OperationalException(f'Exchange {name} is not supported')
try: try:
api = getattr(ccxt, name.lower())({ api = getattr(ccxt_module, name.lower())({
'apiKey': exchange_config.get('key'), 'apiKey': exchange_config.get('key'),
'secret': exchange_config.get('secret'), 'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'), 'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''), 'uid': exchange_config.get('uid', ''),
'enableRateLimit': True, 'enableRateLimit': exchange_config.get('ccxt_rate_limit', True)
}) })
except (KeyError, AttributeError): except (KeyError, AttributeError):
raise OperationalException(f'Exchange {name} is not supported') raise OperationalException(f'Exchange {name} is not supported')
self.set_sandbox(api, exchange_config, name)
return api return api
@property @property
@@ -103,6 +149,35 @@ class Exchange(object):
"""exchange ccxt id""" """exchange ccxt id"""
return self._api.id return self._api.id
def set_sandbox(self, api, exchange_config: dict, name: str):
if exchange_config.get('sandbox'):
if api.urls.get('test'):
api.urls['api'] = api.urls['test']
logger.info("Enabled Sandbox API on %s", name)
else:
logger.warning(name, "No Sandbox URL in CCXT, exiting. "
"Please check your config.json")
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
def _load_async_markets(self) -> None:
try:
if self._api_async:
asyncio.get_event_loop().run_until_complete(self._api_async.load_markets())
except ccxt.BaseError as e:
logger.warning('Could not load async markets. Reason: %s', e)
return
def _load_markets(self) -> Dict[str, Any]:
""" Initialize markets both sync and async """
try:
markets = self._api.load_markets()
self._load_async_markets()
return markets
except ccxt.BaseError as e:
logger.warning('Unable to initialize markets. Reason: %s', e)
return {}
def validate_pairs(self, pairs: List[str]) -> None: def validate_pairs(self, pairs: List[str]) -> None:
""" """
Checks if all given pairs are tradable on the current exchange. Checks if all given pairs are tradable on the current exchange.
@@ -111,11 +186,9 @@ class Exchange(object):
:return: None :return: None
""" """
try: if not self.markets:
markets = self._api.load_markets() logger.warning('Unable to validate pairs (assuming they are correct).')
except ccxt.BaseError as e: # return
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return
stake_cur = self._conf['stake_currency'] stake_cur = self._conf['stake_currency']
for pair in pairs: for pair in pairs:
@@ -124,10 +197,19 @@ class Exchange(object):
if not pair.endswith(stake_cur): if not pair.endswith(stake_cur):
raise OperationalException( raise OperationalException(
f'Pair {pair} not compatible with stake_currency: {stake_cur}') f'Pair {pair} not compatible with stake_currency: {stake_cur}')
if pair not in markets: if self.markets and pair not in self.markets:
raise OperationalException( raise OperationalException(
f'Pair {pair} is not available at {self.name}') f'Pair {pair} is not available at {self.name}')
def validate_timeframes(self, timeframe: List[str]) -> None:
"""
Checks if ticker interval from config is a supported timeframe on the exchange
"""
timeframes = self._api.timeframes
if timeframe not in timeframes:
raise OperationalException(
f'Invalid ticker {timeframe}, this Exchange supports {timeframes}')
def exchange_has(self, endpoint: str) -> bool: def exchange_has(self, endpoint: str) -> bool:
""" """
Checks if exchange implements a specific API endpoint. Checks if exchange implements a specific API endpoint.
@@ -137,6 +219,28 @@ class Exchange(object):
""" """
return endpoint in self._api.has and self._api.has[endpoint] return endpoint in self._api.has and self._api.has[endpoint]
def symbol_amount_prec(self, pair, amount: float):
'''
Returns the amount to buy or sell to a precision the Exchange accepts
Rounded down
'''
if self._api.markets[pair]['precision']['amount']:
symbol_prec = self._api.markets[pair]['precision']['amount']
big_amount = amount * pow(10, symbol_prec)
amount = floor(big_amount) / pow(10, symbol_prec)
return amount
def symbol_price_prec(self, pair, price: float):
'''
Returns the price buying or selling with to the precision the Exchange accepts
Rounds up
'''
if self._api.markets[pair]['precision']['price']:
symbol_prec = self._api.markets[pair]['precision']['price']
big_price = price * pow(10, symbol_prec)
price = ceil(big_price) / pow(10, symbol_prec)
return price
def buy(self, pair: str, rate: float, amount: float) -> Dict: def buy(self, pair: str, rate: float, amount: float) -> Dict:
if self._conf['dry_run']: if self._conf['dry_run']:
order_id = f'dry_run_buy_{randint(0, 10**6)}' order_id = f'dry_run_buy_{randint(0, 10**6)}'
@@ -154,6 +258,10 @@ class Exchange(object):
return {'id': order_id} return {'id': order_id}
try: try:
# Set the precision for amount and price(rate) as accepted by the exchange
amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate)
return self._api.create_limit_buy_order(pair, amount, rate) return self._api.create_limit_buy_order(pair, amount, rate)
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(
@@ -187,6 +295,10 @@ class Exchange(object):
return {'id': order_id} return {'id': order_id}
try: try:
# Set the precision for amount and price(rate) as accepted by the exchange
amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate)
return self._api.create_limit_sell_order(pair, amount, rate) return self._api.create_limit_sell_order(pair, amount, rate)
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise DependencyException(
@@ -266,15 +378,111 @@ 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 history due to {e.__class__.__name__}. Message: {e}') f'Could not load ticker due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(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, tick_interval: str,
since_ms: int) -> List:
"""
Gets candle history using asyncio and returns the list of candles.
Handles all async doing.
"""
return asyncio.get_event_loop().run_until_complete(
self._async_get_history(pair=pair, tick_interval=tick_interval,
since_ms=since_ms))
async def _async_get_history(self, pair: str,
tick_interval: str,
since_ms: int) -> List:
# Assume exchange returns 500 candles
_LIMIT = 500
one_call = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60 * _LIMIT * 1000
logger.debug("one_call: %s", one_call)
input_coroutines = [self._async_get_candle_history(
pair, tick_interval, since) for since in
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
# Combine tickers
data: List = []
for tick in tickers:
if tick[0] == pair:
data.extend(tick[1])
# Sort data again after extending the result - above calls return in "async order" order
data = sorted(data, key=lambda x: x[0])
logger.info("downloaded %s with length %s.", pair, len(data))
return data
def refresh_tickers(self, pair_list: List[str], ticker_interval: str) -> None:
"""
Refresh tickers asyncronously and return the result.
"""
logger.debug("Refreshing klines for %d pairs", len(pair_list))
asyncio.get_event_loop().run_until_complete(
self.async_get_candles_history(pair_list, ticker_interval))
async def async_get_candles_history(self, pairs: List[str],
tick_interval: str) -> List[Tuple[str, List]]:
"""Download ohlcv history for pair-list asyncronously """
input_coroutines = [self._async_get_candle_history(
symbol, tick_interval) for symbol in pairs]
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
return tickers
@retrier_async
async def _async_get_candle_history(self, pair: str, tick_interval: str,
since_ms: Optional[int] = None) -> Tuple[str, List]:
try:
# fetch ohlcv asynchronously
logger.debug("fetching %s since %s ...", pair, since_ms)
# Calculating ticker interval in second
interval_in_sec = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60
# If (last update time) + (interval in second) is greater or equal than now
# that means we don't have to hit the API as there is no new candle
# so we fetch it from local cache
if (not since_ms and
self._pairs_last_refresh_time.get(pair, 0) + interval_in_sec >=
arrow.utcnow().timestamp):
data = self.klines[pair]
logger.debug("Using cached klines data for %s ...", pair)
else:
data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval,
since=since_ms)
# Because some exchange sort Tickers ASC and other DESC.
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
# when GDAX returns a list of tickers DESC (newest first, oldest last)
data = sorted(data, key=lambda x: x[0])
# keeping last candle time as last refreshed time of the pair
if data:
self._pairs_last_refresh_time[pair] = data[-1][0] // 1000
# keeping candles in cache
self.klines[pair] = data
logger.debug("done fetching %s ...", pair)
return pair, data
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
@retrier @retrier
def get_ticker_history(self, pair: str, tick_interval: str, def get_candle_history(self, pair: str, tick_interval: str,
since_ms: Optional[int] = None) -> List[Dict]: since_ms: Optional[int] = None) -> List[Dict]:
try: try:
# last item should be in the time interval [now - tick_interval, now] # last item should be in the time interval [now - tick_interval, now]
@@ -353,6 +561,37 @@ class Exchange(object):
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
@retrier
def get_order_book(self, pair: str, limit: int = 100) -> dict:
"""
get order book level 2 from exchange
Notes:
20180619: bittrex doesnt support limits -.-
20180619: binance support limits but only on specific range
"""
try:
if self._api.name == 'Binance':
limit_range = [5, 10, 20, 50, 100, 500, 1000]
# get next-higher step in the limit_range list
limit = min(list(filter(lambda x: limit <= x, limit_range)))
# above script works like loop below (but with slightly better performance):
# for limitx in limit_range:
# if limit <= limitx:
# limit = limitx
# break
return self._api.fetch_l2_order_book(pair, limit)
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching order book.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get order book due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(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:
if self._conf['dry_run']: if self._conf['dry_run']:
@@ -360,7 +599,8 @@ class Exchange(object):
if not self.exchange_has('fetchMyTrades'): if not self.exchange_has('fetchMyTrades'):
return [] return []
try: try:
my_trades = self._api.fetch_my_trades(pair, since.timestamp()) # Allow 5s offset to catch slight time offsets (discovered in #1185)
my_trades = self._api.fetch_my_trades(pair, since.timestamp() - 5)
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
@@ -406,12 +646,3 @@ class Exchange(object):
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}')
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) raise OperationalException(e)
def get_amount_lots(self, pair: str, amount: float) -> float:
"""
get buyable amount rounding, ..
"""
# validate that markets are loaded before trying to get fee
if not self._api.markets:
self._api.load_markets()
return self._api.amount_to_lots(pair, amount)

View File

@@ -0,0 +1,58 @@
"""
Functions to analyze ticker data with indicators and produce buy and sell signals
"""
import logging
import pandas as pd
from pandas import DataFrame, to_datetime
logger = logging.getLogger(__name__)
def parse_ticker_dataframe(ticker: list) -> DataFrame:
"""
Analyses the trend for the given ticker history
:param ticker: See exchange.get_candle_history
:return: DataFrame
"""
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
frame = DataFrame(ticker, columns=cols)
frame['date'] = to_datetime(frame['date'],
unit='ms',
utc=True,
infer_datetime_format=True)
# group by index and aggregate results to eliminate duplicate ticks
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'max',
})
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
return frame
def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
"""
Gets order book list, returns dataframe with below format per suggested by creslin
-------------------------------------------------------------------
b_sum b_size bids asks a_size a_sum
-------------------------------------------------------------------
"""
cols = ['bids', 'b_size']
bids_frame = DataFrame(bids, columns=cols)
# add cumulative sum column
bids_frame['b_sum'] = bids_frame['b_size'].cumsum()
cols2 = ['asks', 'a_size']
asks_frame = DataFrame(asks, columns=cols2)
# add cumulative sum column
asks_frame['a_sum'] = asks_frame['a_size'].cumsum()
frame = pd.concat([bids_frame['b_sum'], bids_frame['b_size'], bids_frame['bids'],
asks_frame['asks'], asks_frame['a_size'], asks_frame['a_sum']], axis=1,
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum'])
# logger.info('order book %s', frame )
return frame

View File

@@ -8,9 +8,10 @@ import time
from typing import Dict, List from typing import Dict, List
from coinmarketcap import Market from coinmarketcap import Market
from requests.exceptions import RequestException
from freqtrade.constants import SUPPORTED_FIAT from freqtrade.constants import SUPPORTED_FIAT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -88,10 +89,10 @@ class CryptoToFiatConverter(object):
coinlistings = self._coinmarketcap.listings() coinlistings = self._coinmarketcap.listings()
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])), self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
coinlistings["data"])) coinlistings["data"]))
except (ValueError, RequestException) as exception: except (BaseException) as exception:
logger.error( logger.error(
"Could not load FIAT Cryptocurrency map for the following problem: %s", "Could not load FIAT Cryptocurrency map for the following problem: %s",
exception type(exception).__name__
) )
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:

View File

@@ -7,22 +7,22 @@ import logging
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional, Any, Callable from typing import Any, Callable, Dict, List, Optional
import arrow import arrow
import requests from requests.exceptions import RequestException
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
from freqtrade import ( from freqtrade import (DependencyException, OperationalException,
DependencyException, OperationalException, TemporaryError, persistence, __version__, TemporaryError, __version__, constants, persistence)
)
from freqtrade import constants
from freqtrade.analyze import Analyze
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc_manager import RPCManager from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
from freqtrade.exchange.exchange_helpers import order_book_to_dataframe
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -50,12 +50,10 @@ class FreqtradeBot(object):
# Init objects # Init objects
self.config = config self.config = config
self.analyze = Analyze(self.config) self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.fiat_converter = CryptoToFiatConverter()
self.rpc: RPCManager = RPCManager(self) self.rpc: RPCManager = RPCManager(self)
self.persistence = None self.persistence = None
self.exchange = Exchange(self.config) self.exchange = Exchange(self.config)
self._init_modules() self._init_modules()
def _init_modules(self) -> None: def _init_modules(self) -> None:
@@ -93,8 +91,13 @@ class FreqtradeBot(object):
# Log state transition # Log state transition
state = self.state state = self.state
if state != old_state: if state != old_state:
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`') self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'{state.name.lower()}'
})
logger.info('Changing state to: %s', state.name) logger.info('Changing state to: %s', state.name)
if state == State.RUNNING:
self._startup_messages()
if state == State.STOPPED: if state == State.STOPPED:
time.sleep(1) time.sleep(1)
@@ -111,6 +114,38 @@ class FreqtradeBot(object):
nb_assets=nb_assets) nb_assets=nb_assets)
return state return state
def _startup_messages(self) -> None:
if self.config.get('dry_run', False):
self.rpc.send_msg({
'type': RPCMessageType.WARNING_NOTIFICATION,
'status': 'Dry run is enabled. All trades are simulated.'
})
stake_currency = self.config['stake_currency']
stake_amount = self.config['stake_amount']
minimal_roi = self.config['minimal_roi']
ticker_interval = self.config['ticker_interval']
exchange_name = self.config['exchange']['name']
strategy_name = self.config.get('strategy', '')
self.rpc.send_msg({
'type': RPCMessageType.CUSTOM_NOTIFICATION,
'status': f'*Exchange:* `{exchange_name}`\n'
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
f'*Minimum ROI:* `{minimal_roi}`\n'
f'*Ticker Interval:* `{ticker_interval}`\n'
f'*Strategy:* `{strategy_name}`'
})
if self.config.get('dynamic_whitelist', False):
top_pairs = 'top ' + str(self.config.get('dynamic_whitelist', 20))
specific_pairs = ''
else:
top_pairs = 'whitelisted'
specific_pairs = '\n' + ', '.join(self.config['exchange'].get('pair_whitelist', ''))
self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...'
f'{specific_pairs}'
})
def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any: def _throttle(self, func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
""" """
Throttles the given callable that it Throttles the given callable that it
@@ -147,6 +182,9 @@ class FreqtradeBot(object):
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
self.config['exchange']['pair_whitelist'] = final_list self.config['exchange']['pair_whitelist'] = final_list
# Refreshing candles
self.exchange.refresh_tickers(final_list, self.strategy.ticker_interval)
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
@@ -160,7 +198,7 @@ class FreqtradeBot(object):
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.config['unfilledtimeout']) self.check_handle_timedout()
Trade.session.flush() Trade.session.flush()
except TemporaryError as error: except TemporaryError as error:
@@ -169,9 +207,10 @@ class FreqtradeBot(object):
except OperationalException: except OperationalException:
tb = traceback.format_exc() tb = traceback.format_exc()
hint = 'Issue `/start` if you think it is safe to restart.' hint = 'Issue `/start` if you think it is safe to restart.'
self.rpc.send_msg( self.rpc.send_msg({
f'*Status:* OperationalException:\n```\n{tb}```{hint}' 'type': RPCMessageType.STATUS_NOTIFICATION,
) 'status': f'OperationalException:\n```\n{tb}```{hint}'
})
logger.exception('OperationalException. Stopping trader ...') logger.exception('OperationalException. Stopping trader ...')
self.state = State.STOPPED self.state = State.STOPPED
return state_changed return state_changed
@@ -233,18 +272,47 @@ class FreqtradeBot(object):
return final_list return final_list
def get_target_bid(self, ticker: Dict[str, float]) -> float: def get_target_bid(self, pair: str, ticker: Dict[str, float]) -> float:
""" """
Calculates bid target between current ask price and last price Calculates bid target between current ask price and last price
:param ticker: Ticker to use for getting Ask and Last Price :param ticker: Ticker to use for getting Ask and Last Price
:return: float: Price :return: float: Price
""" """
if ticker['ask'] < ticker['last']: if ticker['ask'] < ticker['last']:
return ticker['ask'] ticker_rate = ticker['ask']
balance = self.config['bid_strategy']['ask_last_balance'] else:
return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) balance = self.config['bid_strategy']['ask_last_balance']
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
used_rate = ticker_rate
config_bid_strategy = self.config.get('bid_strategy', {})
if 'use_order_book' in config_bid_strategy and\
config_bid_strategy.get('use_order_book', False):
logger.info('Getting price from order book')
order_book_top = config_bid_strategy.get('order_book_top', 1)
order_book = self.exchange.get_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book)
# top 1 = index 0
order_book_rate = order_book['bids'][order_book_top - 1][0]
# if ticker has lower rate, then use ticker ( usefull if down trending )
logger.info('...top %s order book buy rate %0.8f', order_book_top, order_book_rate)
if ticker_rate < order_book_rate:
logger.info('...using ticker rate instead %0.8f', ticker_rate)
used_rate = ticker_rate
else:
used_rate = order_book_rate
else:
logger.info('Using Last Ask / Last Price')
used_rate = ticker_rate
return used_rate
def _get_trade_stake_amount(self) -> Optional[float]: def _get_trade_stake_amount(self) -> Optional[float]:
"""
Check if stake amount can be fulfilled with the available balance
for the stake currency
:return: float: Stake Amount
"""
stake_amount = self.config['stake_amount'] stake_amount = self.config['stake_amount']
avaliable_amount = self.exchange.get_balance(self.config['stake_currency']) avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
@@ -277,21 +345,24 @@ class FreqtradeBot(object):
return None return None
min_stake_amounts = [] min_stake_amounts = []
if 'cost' in market['limits'] and 'min' in market['limits']['cost']: limits = market['limits']
min_stake_amounts.append(market['limits']['cost']['min']) if ('cost' in limits and 'min' in limits['cost']
and limits['cost']['min'] is not None):
min_stake_amounts.append(limits['cost']['min'])
if 'amount' in market['limits'] and 'min' in market['limits']['amount']: if ('amount' in limits and 'min' in limits['amount']
min_stake_amounts.append(market['limits']['amount']['min'] * price) and limits['amount']['min'] is not None):
min_stake_amounts.append(limits['amount']['min'] * price)
if not min_stake_amounts: if not min_stake_amounts:
return None return None
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss
if self.analyze.get_stoploss() is not None: if self.strategy.stoploss is not None:
amount_reserve_percent += self.analyze.get_stoploss() amount_reserve_percent += self.strategy.stoploss
# it should not be more than 50% # it should not be more than 50%
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_trade(self) -> bool:
""" """
@@ -299,14 +370,11 @@ class FreqtradeBot(object):
if one pair triggers the buy_signal a new trade record gets created if one pair triggers the buy_signal a new trade record gets created
:return: True if a trade object has been created and persisted, False otherwise :return: True if a trade object has been created and persisted, False otherwise
""" """
interval = self.analyze.get_ticker_interval() interval = self.strategy.ticker_interval
stake_amount = self._get_trade_stake_amount() stake_amount = self._get_trade_stake_amount()
if not stake_amount: if not stake_amount:
return False return False
stake_currency = self.config['stake_currency']
fiat_currency = self.config['fiat_display_currency']
exc_name = self.exchange.name
logger.info( logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...', 'Checking buy signals to create a new trade with stake_amount: %f ...',
@@ -323,19 +391,53 @@ class FreqtradeBot(object):
if not whitelist: if not whitelist:
raise DependencyException('No currency pairs in whitelist') raise DependencyException('No currency pairs in whitelist')
# Pick pair based on buy signals # running get_signal on historical data fetched
# to find buy signals
for _pair in whitelist: for _pair in whitelist:
(buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval) (buy, sell) = self.strategy.get_signal(_pair, interval, self.exchange.klines.get(_pair))
if buy and not sell: if buy and not sell:
pair = _pair bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\
break get('check_depth_of_market', {})
else: if (bidstrat_check_depth_of_market.get('enabled', False)) and\
return False (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):
return self.execute_buy(_pair, stake_amount)
else:
return False
return self.execute_buy(_pair, stake_amount)
return False
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
"""
Checks depth of market before executing a buy
"""
conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
logger.info('checking depth of market for %s', pair)
order_book = self.exchange.get_order_book(pair, 1000)
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
order_book_bids = order_book_data_frame['b_size'].sum()
order_book_asks = order_book_data_frame['a_size'].sum()
bids_ask_delta = order_book_bids / order_book_asks
logger.info('bids: %s, asks: %s, delta: %s', order_book_bids,
order_book_asks, bids_ask_delta)
if bids_ask_delta >= conf_bids_to_ask_delta:
return True
return False
def execute_buy(self, pair: str, stake_amount: float) -> bool:
"""
Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY
:return: None
"""
pair_s = pair.replace('_', '/') pair_s = pair.replace('_', '/')
pair_url = self.exchange.get_pair_detail_url(pair) pair_url = self.exchange.get_pair_detail_url(pair)
stake_currency = self.config['stake_currency']
fiat_currency = self.config.get('fiat_display_currency', None)
# Calculate amount # Calculate amount
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair)) buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair))
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit) min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
if min_stake_amount is not None and min_stake_amount > stake_amount: if min_stake_amount is not None and min_stake_amount > stake_amount:
@@ -349,18 +451,16 @@ class FreqtradeBot(object):
order_id = self.exchange.buy(pair, buy_limit, amount)['id'] order_id = self.exchange.buy(pair, buy_limit, amount)['id']
stake_amount_fiat = self.fiat_converter.convert_amount( self.rpc.send_msg({
stake_amount, 'type': RPCMessageType.BUY_NOTIFICATION,
stake_currency, 'exchange': self.exchange.name.capitalize(),
fiat_currency 'pair': pair_s,
) 'market_url': pair_url,
'limit': buy_limit,
# Create trade entity and return 'stake_amount': stake_amount,
self.rpc.send_msg( 'stake_currency': stake_currency,
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \ 'fiat_currency': fiat_currency
with limit `{buy_limit:.8f} ({stake_amount:.6f} \ })
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
trade = Trade( trade = Trade(
@@ -373,7 +473,9 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
open_rate_requested=buy_limit, open_rate_requested=buy_limit,
open_date=datetime.utcnow(), open_date=datetime.utcnow(),
exchange=self.exchange.id, exchange=self.exchange.id,
open_order_id=order_id open_order_id=order_id,
strategy=self.strategy.get_strategy_name(),
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
) )
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
@@ -478,27 +580,62 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
raise ValueError(f'attempt to handle closed trade: {trade}') raise ValueError(f'attempt to handle closed trade: {trade}')
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
current_rate = self.exchange.get_ticker(trade.pair)['bid'] sell_rate = self.exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False) (buy, sell) = (False, False)
experimental = self.config.get('experimental', {}) experimental = self.config.get('experimental', {})
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'): if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
(buy, sell) = self.analyze.get_signal(self.exchange, ticker = self.exchange.klines.get(trade.pair)
trade.pair, self.analyze.get_ticker_interval()) (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
ticker)
config_ask_strategy = self.config.get('ask_strategy', {})
if config_ask_strategy.get('use_order_book', False):
logger.info('Using order book for selling...')
# logger.debug('Order book %s',orderBook)
order_book_min = config_ask_strategy.get('order_book_min', 1)
order_book_max = config_ask_strategy.get('order_book_max', 1)
order_book = self.exchange.get_order_book(trade.pair, order_book_max)
for i in range(order_book_min, order_book_max + 1):
order_book_rate = order_book['asks'][i - 1][0]
# if orderbook has higher rate (high profit),
# use orderbook, otherwise just use bids rate
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
if sell_rate < order_book_rate:
sell_rate = order_book_rate
if self.check_sell(trade, sell_rate, buy, sell):
return True
break
else:
logger.info('checking sell')
if self.check_sell(trade, sell_rate, buy, sell):
return True
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
self.execute_sell(trade, current_rate)
return True
logger.info('Found no sell signals for whitelisted currencies. Trying again..') logger.info('Found no sell signals for whitelisted currencies. Trying again..')
return False return False
def check_handle_timedout(self, timeoutvalue: int) -> None: def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell)
if should_sell.sell_flag:
self.execute_sell(trade, sell_rate, should_sell.sell_type)
logger.info('excuted sell')
return True
return False
def check_handle_timedout(self) -> None:
""" """
Check if any orders are timed out and cancel if neccessary Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out :param timeoutvalue: Number of minutes until order is considered timed out
:return: None :return: None
""" """
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime buy_timeout = self.config['unfilledtimeout']['buy']
sell_timeout = self.config['unfilledtimeout']['sell']
buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try: try:
@@ -509,7 +646,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
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 requests.exceptions.RequestException: except (RequestException, DependencyException):
logger.info( logger.info(
'Cannot query order for %s due to %s', 'Cannot query order for %s due to %s',
trade, trade,
@@ -521,10 +658,12 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
if int(order['remaining']) == 0: if int(order['remaining']) == 0:
continue continue
if order['side'] == 'buy' and ordertime < timeoutthreashold: # Check if trade is still actually open
self.handle_timedout_limit_buy(trade, order) if order['status'] == 'open':
elif order['side'] == 'sell' and ordertime < timeoutthreashold: if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
self.handle_timedout_limit_sell(trade, order) self.handle_timedout_limit_buy(trade, order)
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
self.handle_timedout_limit_sell(trade, order)
# FIX: 20180110, why is cancel.order unconditionally here, whereas # FIX: 20180110, why is cancel.order unconditionally here, whereas
# it is conditionally called in the # it is conditionally called in the
@@ -540,7 +679,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
Trade.session.delete(trade) Trade.session.delete(trade)
Trade.session.flush() Trade.session.flush()
logger.info('Buy order timeout for %s.', trade) logger.info('Buy order timeout for %s.', trade)
self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled') self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Unfilled buy order for {pair_s} cancelled due to timeout'
})
return True return True
# if trade is partially complete, edit the stake details for the trade # if trade is partially complete, edit the stake details for the trade
@@ -549,7 +691,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade) logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled') self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Remaining buy order for {pair_s} cancelled due to timeout'
})
return False return False
# FIX: 20180110, should cancel_order() be cond. or unconditionally called? # FIX: 20180110, should cancel_order() be cond. or unconditionally called?
@@ -567,65 +712,59 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
trade.close_date = None trade.close_date = None
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled') self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Unfilled sell order for {pair_s} cancelled due to timeout'
})
logger.info('Sell order timeout for %s.', trade) logger.info('Sell order timeout for %s.', trade)
return True return True
# TODO: figure out how to handle partially complete sell orders # TODO: figure out how to handle partially complete sell orders
return False return False
def execute_sell(self, trade: Trade, limit: float) -> None: def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None:
""" """
Executes a limit sell for the given trade and limit Executes a limit sell for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
:param limit: limit rate for the sell order :param limit: limit rate for the sell order
:param sellreason: Reason the sell was triggered
:return: None :return: None
""" """
exc = trade.exchange
pair = trade.pair
# Execute sell and update trade record # Execute sell and update trade record
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id'] order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['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
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit) profit_trade = trade.calc_profit(rate=limit)
current_rate = self.exchange.get_ticker(trade.pair)['bid'] current_rate = self.exchange.get_ticker(trade.pair)['bid']
profit = trade.calc_profit_percent(limit) profit_percent = trade.calc_profit_percent(limit)
pair_url = self.exchange.get_pair_detail_url(trade.pair) pair_url = self.exchange.get_pair_detail_url(trade.pair)
gain = "profit" if fmt_exp_profit > 0 else "loss" gain = "profit" if profit_percent > 0 else "loss"
message = f"*{exc}:* Selling\n" \ msg = {
f"*Current Pair:* [{pair}]({pair_url})\n" \ 'type': RPCMessageType.SELL_NOTIFICATION,
f"*Limit:* `{limit}`\n" \ 'exchange': trade.exchange.capitalize(),
f"*Amount:* `{round(trade.amount, 8)}`\n" \ 'pair': trade.pair,
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \ 'gain': gain,
f"*Current Rate:* `{current_rate:.8f}`\n" \ 'market_url': pair_url,
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \ 'limit': limit,
"" 'amount': trade.amount,
'open_rate': trade.open_rate,
'current_rate': current_rate,
'profit_amount': profit_trade,
'profit_percent': profit_percent,
}
# For regular case, when the configuration exists # For regular case, when the configuration exists
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config: if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
stake = self.config['stake_currency'] stake_currency = self.config['stake_currency']
fiat = self.config['fiat_display_currency'] fiat_currency = self.config['fiat_display_currency']
fiat_converter = CryptoToFiatConverter() msg.update({
profit_fiat = fiat_converter.convert_amount( 'stake_currency': stake_currency,
profit_trade, 'fiat_currency': fiat_currency,
stake, })
fiat
)
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
f'` / {profit_fiat:.3f} {fiat})`'\
''
# Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well
else:
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade
)
# Send the message # Send the message
self.rpc.send_msg(message) self.rpc.send_msg(msg)
Trade.session.flush() Trade.session.flush()

View File

@@ -1,4 +1,4 @@
from math import exp, pi, sqrt, cos from math import cos, exp, pi, sqrt
import numpy as np import numpy as np
import talib as ta import talib as ta

View File

@@ -10,9 +10,10 @@ from typing import List
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration, set_loggers
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.state import State from freqtrade.state import State
from freqtrade.rpc import RPCMessageType
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
@@ -59,7 +60,10 @@ def main(sysargv: List[str]) -> None:
logger.exception('Fatal exception!') logger.exception('Fatal exception!')
finally: finally:
if freqtrade: if freqtrade:
freqtrade.rpc.send_msg('*Status:* `Process died ...`') freqtrade.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'process died'
})
freqtrade.cleanup() freqtrade.cleanup()
sys.exit(return_code) sys.exit(return_code)
@@ -73,24 +77,13 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
# Create new instance # Create new instance
freqtrade = FreqtradeBot(Configuration(args).get_config()) freqtrade = FreqtradeBot(Configuration(args).get_config())
freqtrade.rpc.send_msg( freqtrade.rpc.send_msg({
'*Status:* `Config reloaded ...`'.format( 'type': RPCMessageType.STATUS_NOTIFICATION,
freqtrade.state.name.lower() 'status': 'config reloaded'
) })
)
return freqtrade return freqtrade
def set_loggers() -> None:
"""
Set the logger level for Third party libs
:return: None
"""
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('ccxt.base.exchange').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
if __name__ == '__main__': if __name__ == '__main__':
set_loggers() set_loggers()
main(sys.argv[1:]) main(sys.argv[1:])

View File

@@ -2,10 +2,10 @@
Various tool function for Freqtrade and scripts Various tool function for Freqtrade and scripts
""" """
import gzip
import json import json
import logging import logging
import re import re
import gzip
from datetime import datetime from datetime import datetime
from typing import Dict from typing import Dict

View File

@@ -1,7 +1,13 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import gzip import gzip
import json try:
import ujson as json
_UJSON = True
except ImportError:
# see mypy/issues/1153
import json # type: ignore
_UJSON = False
import logging import logging
import os import os
from typing import Optional, List, Dict, Tuple, Any from typing import Optional, List, Dict, Tuple, Any
@@ -14,6 +20,14 @@ from freqtrade.arguments import TimeRange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def json_load(data):
"""Try to load data with ujson"""
if _UJSON:
return json.load(data, precise_float=True)
else:
return json.load(data)
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]: def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
if not tickerlist: if not tickerlist:
return tickerlist return tickerlist
@@ -54,11 +68,8 @@ def load_tickerdata_file(
:return dict OR empty if unsuccesful :return dict OR empty if unsuccesful
""" """
path = make_testdata_path(datadir) path = make_testdata_path(datadir)
pair_file_string = pair.replace('/', '_') pair_s = pair.replace('/', '_')
file = os.path.join(path, '{pair}-{ticker_interval}.json'.format( file = os.path.join(path, f'{pair_s}-{ticker_interval}.json')
pair=pair_file_string,
ticker_interval=ticker_interval,
))
gzipfile = file + '.gz' gzipfile = file + '.gz'
# If the file does not exist we download it when None is returned. # If the file does not exist we download it when None is returned.
@@ -166,7 +177,7 @@ def load_cached_data_for_updating(filename: str,
# read the cached file # read the cached file
if os.path.isfile(filename): if os.path.isfile(filename):
with open(filename, "rt") as file: with open(filename, "rt") as file:
data = json.load(file) data = json_load(file)
# remove the last item, because we are not sure if it is correct # remove the last item, because we are not sure if it is correct
# it could be fetched when the candle was incompleted # it could be fetched when the candle was incompleted
if data: if data:
@@ -194,19 +205,18 @@ def download_backtesting_testdata(datadir: str,
timerange: Optional[TimeRange] = None) -> None: timerange: Optional[TimeRange] = None) -> None:
""" """
Download the latest ticker intervals from the exchange for the pairs passed in parameters Download the latest ticker intervals from the exchange for the pair passed in parameters
The data is downloaded starting from the last correct ticker interval data that The data is downloaded starting from the last correct ticker interval data that
esists in a cache. If timerange starts earlier than the data in the cache, exists in a cache. If timerange starts earlier than the data in the cache,
the full data will be redownloaded the full data will be redownloaded
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
:param pairs: list of pairs to download :param pair: pair to download
:param tick_interval: ticker interval :param tick_interval: ticker interval
:param timerange: range of time to download :param timerange: range of time to download
:return: None :return: None
""" """
path = make_testdata_path(datadir) path = make_testdata_path(datadir)
filepair = pair.replace("/", "_") filepair = pair.replace("/", "_")
filename = os.path.join(path, f'{filepair}-{tick_interval}.json') filename = os.path.join(path, f'{filepair}-{tick_interval}.json')
@@ -222,8 +232,11 @@ def download_backtesting_testdata(datadir: str,
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')
new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval, # Default since_ms to 30 days if nothing is given
since_ms=since_ms) new_data = exchange.get_history(pair=pair, tick_interval=tick_interval,
since_ms=since_ms if since_ms
else
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]))

View File

@@ -6,21 +6,24 @@ This module contains the backtesting logic
import logging import logging
import operator import operator
from argparse import Namespace from argparse import Namespace
from datetime import datetime from copy import deepcopy
from typing import Dict, Tuple, Any, List, Optional, NamedTuple from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Optional, Tuple
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import constants, DependencyException from freqtrade import DependencyException, constants
from freqtrade.exchange import Exchange
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.exchange import Exchange
from freqtrade.misc import file_dump_json from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -38,6 +41,9 @@ class BacktestResult(NamedTuple):
close_index: int close_index: int
trade_duration: float trade_duration: float
open_at_end: bool open_at_end: bool
open_rate: float
close_rate: float
sell_reason: SellType
class Backtesting(object): class Backtesting(object):
@@ -48,13 +54,9 @@ class Backtesting(object):
backtesting = Backtesting(config) backtesting = Backtesting(config)
backtesting.start() backtesting.start()
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.config = config self.config = config
self.analyze = Analyze(self.config)
self.ticker_interval = self.analyze.strategy.ticker_interval
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
self.populate_buy_trend = self.analyze.populate_buy_trend
self.populate_sell_trend = self.analyze.populate_sell_trend
# Reset keys for backtesting # Reset keys for backtesting
self.config['exchange']['key'] = '' self.config['exchange']['key'] = ''
@@ -62,9 +64,34 @@ class Backtesting(object):
self.config['exchange']['password'] = '' self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = '' self.config['exchange']['uid'] = ''
self.config['dry_run'] = True self.config['dry_run'] = True
self.strategylist: List[IStrategy] = []
if self.config.get('strategy_list', None):
# Force one interval
self.ticker_interval = str(self.config.get('ticker_interval'))
for strat in list(self.config['strategy_list']):
stratconf = deepcopy(self.config)
stratconf['strategy'] = strat
self.strategylist.append(StrategyResolver(stratconf).strategy)
else:
# only one strategy
self.strategylist.append(StrategyResolver(self.config).strategy)
# Load one strategy
self._set_strategy(self.strategylist[0])
self.exchange = Exchange(self.config) self.exchange = Exchange(self.config)
self.fee = self.exchange.get_fee() self.fee = self.exchange.get_fee()
def _set_strategy(self, strategy):
"""
Load strategy into backtesting
"""
self.strategy = strategy
self.ticker_interval = self.config.get('ticker_interval')
self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe
self.advise_buy = strategy.advise_buy
self.advise_sell = strategy.advise_sell
@staticmethod @staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
""" """
@@ -73,31 +100,37 @@ class Backtesting(object):
:return: tuple containing min_date, max_date :return: tuple containing min_date, max_date
""" """
timeframe = [ timeframe = [
(arrow.get(min(frame.date)), arrow.get(max(frame.date))) (arrow.get(frame['date'].min()), arrow.get(frame['date'].max()))
for frame in data.values() for frame in data.values()
] ]
return min(timeframe, key=operator.itemgetter(0))[0], \ return min(timeframe, key=operator.itemgetter(0))[0], \
max(timeframe, key=operator.itemgetter(1))[1] max(timeframe, key=operator.itemgetter(1))[1]
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str: def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame,
skip_nan: bool = False) -> str:
""" """
Generates and returns a text table for the given backtest data and the results dataframe Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str :return: pretty printed table with tabulate as str
""" """
stake_currency = str(self.config.get('stake_currency')) stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.8f', '.1f') floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
tabular_data = [] tabular_data = []
headers = ['pair', 'buy count', 'avg profit %', headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] 'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data: for pair in data:
result = results[results.pair == pair] result = results[results.pair == pair]
if skip_nan and result.profit_abs.isnull().all():
continue
tabular_data.append([ tabular_data.append([
pair, pair,
len(result.index), len(result.index),
result.profit_percent.mean() * 100.0, result.profit_percent.mean() * 100.0,
result.profit_percent.sum() * 100.0,
result.profit_abs.sum(), result.profit_abs.sum(),
result.trade_duration.mean(), str(timedelta(
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
len(result[result.profit_abs > 0]), len(result[result.profit_abs > 0]),
len(result[result.profit_abs < 0]) len(result[result.profit_abs < 0])
]) ])
@@ -107,22 +140,63 @@ class Backtesting(object):
'TOTAL', 'TOTAL',
len(results.index), len(results.index),
results.profit_percent.mean() * 100.0, results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0,
results.profit_abs.sum(), results.profit_abs.sum(),
results.trade_duration.mean(), str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]), len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0]) len(results[results.profit_abs < 0])
]) ])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None: def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generate small table outlining Backtest results
"""
tabular_data = []
headers = ['Sell Reason', 'Count']
for reason, count in results['sell_reason'].value_counts().iteritems():
tabular_data.append([reason.value, count])
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
records = [(trade_entry.pair, trade_entry.profit_percent, def _generate_text_table_strategy(self, all_results: dict) -> str:
trade_entry.open_time.timestamp(), """
trade_entry.close_time.timestamp(), Generate summary table per strategy
trade_entry.open_index - 1, trade_entry.trade_duration) """
for index, trade_entry in results.iterrows()] stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f')
tabular_data = []
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for strategy, results in all_results.items():
tabular_data.append([
strategy,
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0,
results.profit_abs.sum(),
str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: str, results: DataFrame,
strategyname: Optional[str] = None) -> None:
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
if records: if records:
if strategyname:
# Inject strategyname to filename
recname = Path(recordfilename)
recordfilename = str(Path.joinpath(
recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix))
logger.info('Dumping backtest results to %s', recordfilename) logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records) file_dump_json(recordfilename, records)
@@ -133,7 +207,7 @@ class Backtesting(object):
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)
trade = Trade( trade = Trade(
open_rate=buy_row.close, open_rate=buy_row.open,
open_date=buy_row.date, open_date=buy_row.date,
stake_amount=stake_amount, stake_amount=stake_amount,
amount=stake_amount / buy_row.open, amount=stake_amount / buy_row.open,
@@ -148,31 +222,40 @@ class Backtesting(object):
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1 trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1
buy_signal = sell_row.buy buy_signal = sell_row.buy
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal, sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
sell_row.sell): sell_row.sell)
if sell.sell_flag:
return BacktestResult(pair=pair, return BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.close), profit_percent=trade.calc_profit_percent(rate=sell_row.open),
profit_abs=trade.calc_profit(rate=sell_row.close), profit_abs=trade.calc_profit(rate=sell_row.open),
open_time=buy_row.date, open_time=buy_row.date,
close_time=sell_row.date, close_time=sell_row.date,
trade_duration=(sell_row.date - buy_row.date).seconds // 60, trade_duration=int((
sell_row.date - buy_row.date).total_seconds() // 60),
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=False open_at_end=False,
open_rate=buy_row.open,
close_rate=sell_row.open,
sell_reason=sell.sell_type
) )
if partial_ticker: if partial_ticker:
# no sell condition found - trade stil open at end of backtest period # no sell condition found - trade stil open at end of backtest period
sell_row = partial_ticker[-1] sell_row = partial_ticker[-1]
btr = BacktestResult(pair=pair, btr = BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.close), profit_percent=trade.calc_profit_percent(rate=sell_row.open),
profit_abs=trade.calc_profit(rate=sell_row.close), profit_abs=trade.calc_profit(rate=sell_row.open),
open_time=buy_row.date, open_time=buy_row.date,
close_time=sell_row.date, close_time=sell_row.date,
trade_duration=(sell_row.date - buy_row.date).seconds // 60, trade_duration=int((
sell_row.date - buy_row.date).total_seconds() // 60),
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=True open_at_end=True,
open_rate=buy_row.open,
close_rate=sell_row.open,
sell_reason=SellType.FORCE_SELL
) )
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair, logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
btr.profit_percent, btr.profit_abs) btr.profit_percent, btr.profit_abs)
@@ -191,20 +274,20 @@ class Backtesting(object):
stake_amount: btc amount to use for each trade stake_amount: btc amount to use for each trade
processed: a processed dictionary with format {pair, data} processed: a processed dictionary with format {pair, data}
max_open_trades: maximum number of concurrent trades (default: 0, disabled) max_open_trades: maximum number of concurrent trades (default: 0, disabled)
realistic: do we try to simulate realistic trades? (default: True) position_stacking: do we allow position stacking? (default: False)
:return: DataFrame :return: DataFrame
""" """
headers = ['date', 'buy', 'open', 'close', 'sell'] headers = ['date', 'buy', 'open', 'close', 'sell']
processed = args['processed'] processed = args['processed']
max_open_trades = args.get('max_open_trades', 0) max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', False) position_stacking = args.get('position_stacking', False)
trades = [] trades = []
trade_count_lock: Dict = {} trade_count_lock: Dict = {}
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = self.populate_sell_trend( ticker_data = self.advise_sell(
self.populate_buy_trend(pair_data))[headers].copy() self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
# to avoid using data from future, we buy/sell with signal from previous candle # to avoid using data from future, we buy/sell with signal from previous candle
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
@@ -221,7 +304,7 @@ class Backtesting(object):
if row.buy == 0 or row.sell == 1: if row.buy == 0 or row.sell == 1:
continue # skip rows where no buy signal or that would immediately sell off continue # skip rows where no buy signal or that would immediately sell off
if realistic: if not position_stacking:
if lock_pair_until is not None and row.date <= lock_pair_until: if lock_pair_until is not None and row.date <= lock_pair_until:
continue continue
if max_open_trades > 0: if max_open_trades > 0:
@@ -249,15 +332,15 @@ class Backtesting(object):
Run a backtesting end-to-end Run a backtesting end-to-end
:return: None :return: None
""" """
data = {} data: Dict[str, Any] = {}
pairs = self.config['exchange']['pair_whitelist'] pairs = self.config['exchange']['pair_whitelist']
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'])
if self.config.get('live'): if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...') logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs: self.exchange.refresh_tickers(pairs, self.ticker_interval)
data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval) data = self.exchange.klines
else: else:
logger.info('Using local backtesting data (using whitelist in given config) ...') logger.info('Using local backtesting data (using whitelist in given config) ...')
@@ -275,58 +358,61 @@ class Backtesting(object):
if not data: if not data:
logger.critical("No data found. Terminating.") logger.critical("No data found. Terminating.")
return return
# Ignore max_open_trades in backtesting, except realistic flag was passed # Use max_open_trades in backtesting, except --disable-max-market-positions is set
if self.config.get('realistic_simulation', False): if self.config.get('use_max_market_positions', True):
max_open_trades = self.config['max_open_trades'] max_open_trades = self.config['max_open_trades']
else: else:
logger.info('Ignoring max_open_trades (realistic_simulation not set) ...') logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
max_open_trades = 0 max_open_trades = 0
all_results = {}
preprocessed = self.tickerdata_to_dataframe(data) for strat in self.strategylist:
logger.info("Running backtesting for Strategy %s", strat.get_strategy_name())
self._set_strategy(strat)
# Print timeframe # need to reprocess data every time to populate signals
min_date, max_date = self.get_timeframe(preprocessed) preprocessed = self.tickerdata_to_dataframe(data)
logger.info(
'Measuring data from %s up to %s (%s days)..',
min_date.isoformat(),
max_date.isoformat(),
(max_date - min_date).days
)
# Execute backtest and print results # Print timeframe
results = self.backtest( min_date, max_date = self.get_timeframe(preprocessed)
{ logger.info(
'stake_amount': self.config.get('stake_amount'), 'Measuring data from %s up to %s (%s days)..',
'processed': preprocessed, min_date.isoformat(),
'max_open_trades': max_open_trades, max_date.isoformat(),
'realistic': self.config.get('realistic_simulation', False), (max_date - min_date).days
}
)
if self.config.get('export', False):
self._store_backtest_result(self.config.get('exportfilename'), results)
logger.info(
'\n======================================== '
'BACKTESTING REPORT'
' =========================================\n'
'%s',
self._generate_text_table(
data,
results
) )
)
logger.info( # Execute backtest and print results
'\n====================================== ' all_results[self.strategy.get_strategy_name()] = self.backtest(
'LEFT OPEN TRADES REPORT' {
' ======================================\n' 'stake_amount': self.config.get('stake_amount'),
'%s', 'processed': preprocessed,
self._generate_text_table( 'max_open_trades': max_open_trades,
data, 'position_stacking': self.config.get('position_stacking', False),
results.loc[results.open_at_end] }
) )
)
for strategy, results in all_results.items():
if self.config.get('export', False):
self._store_backtest_result(self.config['exportfilename'], results,
strategy if len(self.strategylist) > 1 else None)
print(f"Result for strategy {strategy}")
print(' BACKTESTING REPORT '.center(119, '='))
print(self._generate_text_table(data, results))
print(' SELL REASON STATS '.center(119, '='))
print(self._generate_text_table_sell_reason(data, results))
print(' LEFT OPEN TRADES REPORT '.center(119, '='))
print(self._generate_text_table(data, results.loc[results.open_at_end], True))
print()
if len(all_results) > 1:
# Print Strategy summary table
print(' Strategy Summary '.center(119, '='))
print(self._generate_text_table_strategy(all_results))
print('\nFor more details, please look at the detail tables above')
def setup_configuration(args: Namespace) -> Dict[str, Any]: def setup_configuration(args: Namespace) -> Dict[str, Any]:

View File

@@ -4,22 +4,21 @@
This module contains the hyperopt logic This module contains the hyperopt logic
""" """
import json
import logging import logging
import multiprocessing
import os import os
import pickle
import signal
import sys import sys
from argparse import Namespace from argparse import Namespace
from functools import reduce from functools import reduce
from math import exp from math import exp
from operator import itemgetter from operator import itemgetter
from typing import Dict, Any, Callable, Optional from typing import Any, Callable, Dict, List
import numpy
import talib.abstract as ta import talib.abstract as ta
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
from pandas import DataFrame from pandas import DataFrame
from sklearn.externals.joblib import Parallel, delayed, dump, load
from skopt import Optimizer
from skopt.space import Categorical, Dimension, Integer, Real
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
@@ -29,6 +28,9 @@ from freqtrade.optimize.backtesting import Backtesting
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
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')
class Hyperopt(Backtesting): class Hyperopt(Backtesting):
""" """
@@ -44,7 +46,6 @@ class Hyperopt(Backtesting):
# to the number of days # to the number of days
self.target_trades = 600 self.target_trades = 600
self.total_tries = config.get('epochs', 0) self.total_tries = config.get('epochs', 0)
self.current_tries = 0
self.current_best_loss = 100 self.current_best_loss = 100
# max average trade duration in minutes # max average trade duration in minutes
@@ -56,130 +57,38 @@ class Hyperopt(Backtesting):
# check that the reported Σ% values do not exceed this! # check that the reported Σ% values do not exceed this!
self.expected_max_profit = 3.0 self.expected_max_profit = 3.0
# Configuration and data used by hyperopt # Previous evaluations
self.processed: Optional[Dict[str, Any]] = None self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle')
self.trials: List = []
# Hyperopt Trials def get_args(self, params):
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle') dimensions = self.hyperopt_space()
self.trials = Trials() # Ensure the number of dimensions match
# the number of parameters in the list x.
if len(params) != len(dimensions):
raise ValueError('Mismatch in number of search-space dimensions. '
f'len(dimensions)=={len(dimensions)} and len(x)=={len(params)}')
# Create a dict where the keys are the names of the dimensions
# and the values are taken from the list of parameters x.
arg_dict = {dim.name: value for dim, value in zip(dimensions, params)}
return arg_dict
@staticmethod @staticmethod
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
"""
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
dataframe['cci'] = ta.CCI(dataframe)
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'] dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
dataframe['mfi'] = ta.MFI(dataframe) dataframe['mfi'] = ta.MFI(dataframe)
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['roc'] = ta.ROC(dataframe)
dataframe['rsi'] = ta.RSI(dataframe) dataframe['rsi'] = ta.RSI(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
# Stoch fast
stoch_fast = ta.STOCHF(dataframe) stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk'] dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
# 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_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
# EMA - Exponential Moving Average
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabolic
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
"""
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
return dataframe return dataframe
@@ -187,15 +96,16 @@ class Hyperopt(Backtesting):
""" """
Save hyperopt trials to file Save hyperopt trials to file
""" """
logger.info('Saving Trials to \'%s\'', self.trials_file) if self.trials:
pickle.dump(self.trials, open(self.trials_file, 'wb')) logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file)
dump(self.trials, self.trials_file)
def read_trials(self) -> Trials: def read_trials(self) -> List:
""" """
Read hyperopt trials file Read hyperopt trials file
""" """
logger.info('Reading Trials from \'%s\'', self.trials_file) logger.info('Reading Trials from \'%s\'', self.trials_file)
trials = pickle.load(open(self.trials_file, 'rb')) trials = load(self.trials_file)
os.remove(self.trials_file) os.remove(self.trials_file)
return trials return trials
@@ -203,22 +113,27 @@ class Hyperopt(Backtesting):
""" """
Display Best hyperopt result Display Best hyperopt result
""" """
vals = json.dumps(self.trials.best_trial['misc']['vals'], indent=4) results = sorted(self.trials, key=itemgetter('loss'))
results = self.trials.best_trial['result']['result'] best_result = results[0]
logger.info('Best result:\n%s\nwith values:\n%s', results, vals) logger.info(
'Best result:\n%s\nwith values:\n%s',
best_result['result'],
best_result['params']
)
if 'roi_t1' in best_result['params']:
logger.info('ROI table:\n%s', self.generate_roi_table(best_result['params']))
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
""" """
if results['loss'] < self.current_best_loss: if results['loss'] < self.current_best_loss:
current = results['current_tries']
total = results['total_tries']
res = results['result']
loss = results['loss']
self.current_best_loss = results['loss'] self.current_best_loss = results['loss']
log_msg = '\n{:5d}/{}: {}. Loss {:.5f}'.format( log_msg = f'\n{current:5d}/{total}: {res}. Loss {loss:.5f}'
results['current_tries'],
results['total_tries'],
results['result'],
results['loss']
)
print(log_msg) print(log_msg)
else: else:
print('.', end='') print('.', end='')
@@ -231,12 +146,13 @@ class Hyperopt(Backtesting):
trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8) trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / self.expected_max_profit) profit_loss = max(0, 1 - total_profit / self.expected_max_profit)
duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1) duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1)
return trade_loss + profit_loss + duration_loss result = trade_loss + profit_loss + duration_loss
return result
@staticmethod @staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> Dict[int, float]:
""" """
Generate the ROI table thqt will be used by Hyperopt Generate the ROI table that will be used by Hyperopt
""" """
roi_table = {} roi_table = {}
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
@@ -247,87 +163,44 @@ class Hyperopt(Backtesting):
return roi_table return roi_table
@staticmethod @staticmethod
def roi_space() -> Dict[str, Any]: def roi_space() -> List[Dimension]:
""" """
Values to search for each ROI steps Values to search for each ROI steps
""" """
return { return [
'roi_t1': hp.quniform('roi_t1', 10, 120, 20), Integer(10, 120, name='roi_t1'),
'roi_t2': hp.quniform('roi_t2', 10, 60, 15), Integer(10, 60, name='roi_t2'),
'roi_t3': hp.quniform('roi_t3', 10, 40, 10), Integer(10, 40, name='roi_t3'),
'roi_p1': hp.quniform('roi_p1', 0.01, 0.04, 0.01), Real(0.01, 0.04, name='roi_p1'),
'roi_p2': hp.quniform('roi_p2', 0.01, 0.07, 0.01), Real(0.01, 0.07, name='roi_p2'),
'roi_p3': hp.quniform('roi_p3', 0.01, 0.20, 0.01), Real(0.01, 0.20, name='roi_p3'),
} ]
@staticmethod @staticmethod
def stoploss_space() -> Dict[str, Any]: def stoploss_space() -> List[Dimension]:
""" """
Stoploss Value to search Stoploss search space
""" """
return { return [
'stoploss': hp.quniform('stoploss', -0.5, -0.02, 0.02), Real(-0.5, -0.02, name='stoploss'),
} ]
@staticmethod @staticmethod
def indicator_space() -> Dict[str, Any]: def indicator_space() -> List[Dimension]:
""" """
Define your Hyperopt space for searching strategy parameters Define your Hyperopt space for searching strategy parameters
""" """
return { return [
'macd_below_zero': hp.choice('macd_below_zero', [ Integer(10, 25, name='mfi-value'),
{'enabled': False}, Integer(15, 45, name='fastd-value'),
{'enabled': True} Integer(20, 50, name='adx-value'),
]), Integer(20, 40, name='rsi-value'),
'mfi': hp.choice('mfi', [ Categorical([True, False], name='mfi-enabled'),
{'enabled': False}, Categorical([True, False], name='fastd-enabled'),
{'enabled': True, 'value': hp.quniform('mfi-value', 10, 25, 5)} Categorical([True, False], name='adx-enabled'),
]), Categorical([True, False], name='rsi-enabled'),
'fastd': hp.choice('fastd', [ Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
{'enabled': False}, ]
{'enabled': True, 'value': hp.quniform('fastd-value', 15, 45, 5)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 20, 50, 5)}
]),
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 5)}
]),
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
{'enabled': False},
{'enabled': True}
]),
'over_sar': hp.choice('over_sar', [
{'enabled': False},
{'enabled': True}
]),
'green_candle': hp.choice('green_candle', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_sma': hp.choice('uptrend_sma', [
{'enabled': False},
{'enabled': True}
]),
'trigger': hp.choice('trigger', [
{'type': 'lower_bb'},
{'type': 'lower_bb_tema'},
{'type': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema3_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'ht_sine'},
{'type': 'heiken_reversal_bull'},
{'type': 'di_cross'},
]),
}
def has_space(self, space: str) -> bool: def has_space(self, space: str) -> bool:
""" """
@@ -337,17 +210,17 @@ class Hyperopt(Backtesting):
return True return True
return False return False
def hyperopt_space(self) -> Dict[str, Any]: def hyperopt_space(self) -> List[Dimension]:
""" """
Return the space to use during Hyperopt Return the space to use during Hyperopt
""" """
spaces: Dict = {} spaces: List[Dimension] = []
if self.has_space('buy'): if self.has_space('buy'):
spaces = {**spaces, **Hyperopt.indicator_space()} spaces += Hyperopt.indicator_space()
if self.has_space('roi'): if self.has_space('roi'):
spaces = {**spaces, **Hyperopt.roi_space()} spaces += Hyperopt.roi_space()
if self.has_space('stoploss'): if self.has_space('stoploss'):
spaces = {**spaces, **Hyperopt.stoploss_space()} spaces += Hyperopt.stoploss_space()
return spaces return spaces
@staticmethod @staticmethod
@@ -355,69 +228,32 @@ class Hyperopt(Backtesting):
""" """
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) -> 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 'uptrend_long_ema' in params and params['uptrend_long_ema']['enabled']: if 'mfi-enabled' in params and params['mfi-enabled']:
conditions.append(dataframe['ema50'] > dataframe['ema100']) conditions.append(dataframe['mfi'] < params['mfi-value'])
if 'macd_below_zero' in params and params['macd_below_zero']['enabled']: if 'fastd-enabled' in params and params['fastd-enabled']:
conditions.append(dataframe['macd'] < 0) conditions.append(dataframe['fastd'] < params['fastd-value'])
if 'uptrend_short_ema' in params and params['uptrend_short_ema']['enabled']: if 'adx-enabled' in params and params['adx-enabled']:
conditions.append(dataframe['ema5'] > dataframe['ema10']) conditions.append(dataframe['adx'] > params['adx-value'])
if 'mfi' in params and params['mfi']['enabled']: if 'rsi-enabled' in params and params['rsi-enabled']:
conditions.append(dataframe['mfi'] < params['mfi']['value']) conditions.append(dataframe['rsi'] < params['rsi-value'])
if 'fastd' in params and params['fastd']['enabled']:
conditions.append(dataframe['fastd'] < params['fastd']['value'])
if 'adx' in params and params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if 'rsi' in params and params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
if 'over_sar' in params and params['over_sar']['enabled']:
conditions.append(dataframe['close'] > dataframe['sar'])
if 'green_candle' in params and params['green_candle']['enabled']:
conditions.append(dataframe['close'] > dataframe['open'])
if 'uptrend_sma' in params and params['uptrend_sma']['enabled']:
prevsma = dataframe['sma'].shift(1)
conditions.append(dataframe['sma'] > prevsma)
# TRIGGERS # TRIGGERS
triggers = { if params['trigger'] == 'bb_lower':
'lower_bb': ( conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
dataframe['close'] < dataframe['bb_lowerband'] if params['trigger'] == 'macd_cross_signal':
), conditions.append(qtpylib.crossed_above(
'lower_bb_tema': (
dataframe['tema'] < dataframe['bb_lowerband']
),
'faststoch10': (qtpylib.crossed_above(
dataframe['fastd'], 10.0
)),
'ao_cross_zero': (qtpylib.crossed_above(
dataframe['ao'], 0.0
)),
'ema3_cross_ema10': (qtpylib.crossed_above(
dataframe['ema3'], dataframe['ema10']
)),
'macd_cross_signal': (qtpylib.crossed_above(
dataframe['macd'], dataframe['macdsignal'] dataframe['macd'], dataframe['macdsignal']
)), ))
'sar_reversal': (qtpylib.crossed_above( if params['trigger'] == 'sar_reversal':
conditions.append(qtpylib.crossed_above(
dataframe['close'], dataframe['sar'] dataframe['close'], dataframe['sar']
)), ))
'ht_sine': (qtpylib.crossed_above(
dataframe['htleadsine'], dataframe['htsine']
)),
'heiken_reversal_bull': (
(qtpylib.crossed_above(dataframe['ha_close'], dataframe['ha_open'])) &
(dataframe['ha_low'] == dataframe['ha_open'])
),
'di_cross': (qtpylib.crossed_above(
dataframe['plus_di'], dataframe['minus_di']
)),
}
conditions.append(triggers.get(params['trigger']['type']))
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),
@@ -427,21 +263,24 @@ class Hyperopt(Backtesting):
return populate_buy_trend return populate_buy_trend
def generate_optimizer(self, params: Dict) -> Dict: def generate_optimizer(self, _params) -> Dict:
params = self.get_args(_params)
if self.has_space('roi'): if self.has_space('roi'):
self.analyze.strategy.minimal_roi = self.generate_roi_table(params) self.strategy.minimal_roi = self.generate_roi_table(params)
if self.has_space('buy'): if self.has_space('buy'):
self.populate_buy_trend = self.buy_strategy_generator(params) self.advise_buy = self.buy_strategy_generator(params)
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.analyze.strategy.stoploss = params['stoploss'] self.strategy.stoploss = params['stoploss']
processed = load(TICKERDATA_PICKLE)
results = self.backtest( results = self.backtest(
{ {
'stake_amount': self.config['stake_amount'], 'stake_amount': self.config['stake_amount'],
'processed': self.processed, 'processed': processed,
'realistic': self.config.get('realistic_simulation', False), 'position_stacking': self.config.get('position_stacking', True),
} }
) )
result_explanation = self.format_results(results) result_explanation = self.format_results(results)
@@ -450,30 +289,18 @@ class Hyperopt(Backtesting):
trade_count = len(results.index) trade_count = len(results.index)
trade_duration = results.trade_duration.mean() trade_duration = results.trade_duration.mean()
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration: if trade_count == 0:
print('.', end='')
sys.stdout.flush()
return { return {
'status': STATUS_FAIL, 'loss': MAX_LOSS,
'loss': float('inf') 'params': params,
'result': result_explanation,
} }
loss = self.calculate_loss(total_profit, trade_count, trade_duration) loss = self.calculate_loss(total_profit, trade_count, trade_duration)
self.current_tries += 1
self.log_results(
{
'loss': loss,
'current_tries': self.current_tries,
'total_tries': self.total_tries,
'result': result_explanation,
}
)
return { return {
'loss': loss, 'loss': loss,
'status': STATUS_OK, 'params': params,
'result': result_explanation, 'result': result_explanation,
} }
@@ -481,15 +308,37 @@ class Hyperopt(Backtesting):
""" """
Return the format result in a string Return the format result in a string
""" """
return ('{:6d} trades. Avg profit {: 5.2f}%. ' trades = len(results.index)
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format( avg_profit = results.profit_percent.mean() * 100.0
len(results.index), total_profit = results.profit_abs.sum()
results.profit_percent.mean() * 100.0, stake_cur = self.config['stake_currency']
results.profit_abs.sum(), profit = results.profit_percent.sum()
self.config['stake_currency'], duration = results.trade_duration.mean()
results.profit_percent.sum(),
results.trade_duration.mean(), return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. '
) f'Total profit {total_profit: 11.8f} {stake_cur} '
f'({profit:.4f}Σ%). Avg duration {duration:5.1f} mins.')
def get_optimizer(self, cpu_count) -> Optimizer:
return Optimizer(
self.hyperopt_space(),
base_estimator="ET",
acq_optimizer="auto",
n_initial_points=30,
acq_optimizer_kwargs={'n_jobs': cpu_count}
)
def run_optimizer_parallel(self, parallel, asked) -> List:
return parallel(delayed(self.generate_optimizer)(v) for v in asked)
def load_previous_results(self):
""" read trials file if we have one """
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
self.trials = self.read_trials()
logger.info(
'Loaded %d previous evaluations from disk.',
len(self.trials)
)
def start(self) -> None: def start(self) -> None:
timerange = Arguments.parse_timerange(None if self.config.get( timerange = Arguments.parse_timerange(None if self.config.get(
@@ -502,68 +351,36 @@ class Hyperopt(Backtesting):
) )
if self.has_space('buy'): if self.has_space('buy'):
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore self.strategy.advise_indicators = Hyperopt.populate_indicators # type: ignore
self.processed = self.tickerdata_to_dataframe(data) dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
self.exchange = None # type: ignore
self.load_previous_results()
logger.info('Preparing Trials..') cpus = multiprocessing.cpu_count()
signal.signal(signal.SIGINT, self.signal_handler) logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
# read trials file if we have one
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
self.trials = self.read_trials()
self.current_tries = len(self.trials.results)
self.total_tries += self.current_tries
logger.info(
'Continuing with trials. Current: %d, Total: %d',
self.current_tries,
self.total_tries
)
opt = self.get_optimizer(cpus)
EVALS = max(self.total_tries // cpus, 1)
try: try:
best_parameters = fmin( with Parallel(n_jobs=cpus) as parallel:
fn=self.generate_optimizer, for i in range(EVALS):
space=self.hyperopt_space(), asked = opt.ask(n_points=cpus)
algo=tpe.suggest, f_val = self.run_optimizer_parallel(parallel, asked)
max_evals=self.total_tries, opt.tell(asked, [i['loss'] for i in f_val])
trials=self.trials
)
results = sorted(self.trials.results, key=itemgetter('loss')) self.trials += f_val
best_result = results[0]['result'] for j in range(cpus):
self.log_results({
except ValueError: 'loss': f_val[j]['loss'],
best_parameters = {} 'current_tries': i * cpus + j,
best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \ 'total_tries': self.total_tries,
'try with more epochs (param: -e).' 'result': f_val[j]['result'],
})
# Improve best parameter logging display except KeyboardInterrupt:
if best_parameters: print('User interrupted..')
best_parameters = space_eval(
self.hyperopt_space(),
best_parameters
)
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
if 'roi_t1' in best_parameters:
logger.info('ROI table:\n%s', self.generate_roi_table(best_parameters))
logger.info('Best Result:\n%s', best_result)
# Store trials result to file to resume next time
self.save_trials()
def signal_handler(self, sig, frame) -> None:
"""
Hyperopt SIGINT handler
"""
logger.info(
'Hyperopt received %s',
signal.Signals(sig).name
)
self.save_trials() self.save_trials()
self.log_trials_result() self.log_trials_result()
sys.exit(0)
def start(args: Namespace) -> None: def start(args: Namespace) -> None:
@@ -585,6 +402,13 @@ def start(args: Namespace) -> None:
config['exchange']['key'] = '' config['exchange']['key'] = ''
config['exchange']['secret'] = '' config['exchange']['secret'] = ''
if config.get('strategy') and config.get('strategy') != 'DefaultStrategy':
logger.error("Please don't use --strategy for hyperopt.")
logger.error(
"Read the documentation at "
"https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md "
"to understand how to configure hyperopt.")
raise ValueError("--strategy configured but not supported for hyperopt")
# Initialize backtesting object # Initialize backtesting object
hyperopt = Hyperopt(config) hyperopt = Hyperopt(config)
hyperopt.start() hyperopt.start()

View File

@@ -5,12 +5,11 @@ This module contains the class to persist trades into SQLite
import logging import logging
from datetime import datetime from datetime import datetime
from decimal import Decimal, getcontext from decimal import Decimal, getcontext
from typing import Dict, Optional, Any from typing import Any, Dict, Optional
import arrow import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
create_engine) create_engine, inspect)
from sqlalchemy import inspect
from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
@@ -22,6 +21,7 @@ from freqtrade import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_DECL_BASE: Any = declarative_base() _DECL_BASE: Any = declarative_base()
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
def init(config: Dict) -> None: def init(config: Dict) -> None:
@@ -46,10 +46,8 @@ def init(config: Dict) -> None:
try: try:
engine = create_engine(db_url, **kwargs) engine = create_engine(db_url, **kwargs)
except NoSuchModuleError: except NoSuchModuleError:
error = 'Given value for db_url: \'{}\' is no valid database URL! (See {}).'.format( raise OperationalException(f'Given value for db_url: \'{db_url}\' '
db_url, 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' f'is no valid database URL! (See {_SQL_DOCS_URL})')
)
raise OperationalException(error)
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session() Trade.session = session()
@@ -66,6 +64,10 @@ def has_column(columns, searchname: str) -> bool:
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1 return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
def get_column_def(columns, column: str, default: str) -> str:
return default if not has_column(columns, column) else column
def check_migrate(engine) -> None: def check_migrate(engine) -> None:
""" """
Checks if migration is necessary and migrates if necessary Checks if migration is necessary and migrates if necessary
@@ -73,18 +75,40 @@ def check_migrate(engine) -> None:
inspector = inspect(engine) inspector = inspect(engine)
cols = inspector.get_columns('trades') cols = inspector.get_columns('trades')
tabs = inspector.get_table_names()
table_back_name = 'trades_bak'
for i, table_back_name in enumerate(tabs):
table_back_name = f'trades_bak{i}'
logger.debug(f'trying {table_back_name}')
# Check for latest column
if not has_column(cols, 'ticker_interval'):
logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee')
fee_close = get_column_def(cols, 'fee_close', 'fee')
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
max_rate = get_column_def(cols, 'max_rate', '0.0')
sell_reason = get_column_def(cols, 'sell_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null')
ticker_interval = get_column_def(cols, 'ticker_interval', 'null')
if not has_column(cols, 'fee_open'):
# Schema migration necessary # Schema migration necessary
engine.execute("alter table trades rename to trades_bak") engine.execute(f"alter table trades rename to {table_back_name}")
# let SQLAlchemy create the schema as required # let SQLAlchemy create the schema as required
_DECL_BASE.metadata.create_all(engine) _DECL_BASE.metadata.create_all(engine)
# Copy data back - following the correct schema # Copy data back - following the correct schema
engine.execute("""insert into trades engine.execute(f"""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate, (id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit, open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id) stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, initial_stop_loss, max_rate, sell_reason, strategy,
ticker_interval
)
select id, lower(exchange), select id, lower(exchange),
case case
when instr(pair, '_') != 0 then when instr(pair, '_') != 0 then
@@ -93,22 +117,20 @@ def check_migrate(engine) -> None:
else pair else pair
end end
pair, pair,
is_open, fee fee_open, fee fee_close, is_open, {fee_open} fee_open, {fee_close} fee_close,
open_rate, null open_rate_requested, close_rate, open_rate, {open_rate_requested} open_rate_requested, close_rate,
null close_rate_requested, close_profit, {close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id stake_amount, amount, open_date, close_date, open_order_id,
from trades_bak {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
{max_rate} max_rate, {sell_reason} sell_reason, {strategy} strategy,
{ticker_interval} ticker_interval
from {table_back_name}
""") """)
# Reread columns - the above recreated the table! # Reread columns - the above recreated the table!
inspector = inspect(engine) inspector = inspect(engine)
cols = inspector.get_columns('trades') cols = inspector.get_columns('trades')
if not has_column(cols, 'open_rate_requested'):
engine.execute("alter table trades add open_rate_requested float")
if not has_column(cols, 'close_rate_requested'):
engine.execute("alter table trades add close_rate_requested float")
def cleanup() -> None: def cleanup() -> None:
""" """
@@ -137,8 +159,8 @@ class Trade(_DECL_BASE):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
exchange = Column(String, nullable=False) exchange = Column(String, nullable=False)
pair = Column(String, nullable=False) pair = Column(String, nullable=False, index=True)
is_open = Column(Boolean, nullable=False, default=True) is_open = Column(Boolean, nullable=False, default=True, index=True)
fee_open = Column(Float, nullable=False, default=0.0) fee_open = Column(Float, nullable=False, default=0.0)
fee_close = Column(Float, nullable=False, default=0.0) fee_close = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float) open_rate = Column(Float)
@@ -151,15 +173,60 @@ class Trade(_DECL_BASE):
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime) close_date = Column(DateTime)
open_order_id = Column(String) open_order_id = Column(String)
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
sell_reason = Column(String, nullable=True)
strategy = Column(String, nullable=True)
ticker_interval = Column(Integer, nullable=True)
def __repr__(self): def __repr__(self):
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format( open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed'
self.id,
self.pair, return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
self.amount, f'open_rate={self.open_rate:.8f}, open_since={open_since})')
self.open_rate,
arrow.get(self.open_date).humanize() if self.is_open else 'closed' def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False):
) """this adjusts the stop loss to it's most recently observed setting"""
if initial and not (self.stop_loss is None or self.stop_loss == 0):
# Don't modify if called with initial and nothing to do
return
new_loss = float(current_price * (1 - abs(stoploss)))
# keeping track of the highest observed rate for this trade
if self.max_rate is None:
self.max_rate = current_price
else:
if current_price > self.max_rate:
self.max_rate = current_price
# no stop loss assigned yet
if not self.stop_loss:
logger.debug("assigning new stop loss")
self.stop_loss = new_loss
self.initial_stop_loss = new_loss
# evaluate if the stop loss needs to be updated
else:
if new_loss > self.stop_loss: # stop losses only walk up, never down!
self.stop_loss = new_loss
logger.debug("adjusted stop loss")
else:
logger.debug("keeping current stop loss")
logger.debug(
f"{self.pair} - current price {current_price:.8f}, "
f"bought at {self.open_rate:.8f} and calculated "
f"stop loss is at: {self.initial_stop_loss:.8f} initial "
f"stop at {self.stop_loss:.8f}. "
f"trailing stop loss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f} "
f"and max observed rate was {self.max_rate:.8f}")
def update(self, order: Dict) -> None: def update(self, order: Dict) -> None:
""" """
@@ -167,6 +234,7 @@ class Trade(_DECL_BASE):
:param order: order retrieved by exchange.get_order() :param order: order retrieved by exchange.get_order()
:return: None :return: None
""" """
order_type = order['type']
# Ignore open and cancelled orders # Ignore open and cancelled orders
if order['status'] == 'open' or order['price'] is None: if order['status'] == 'open' or order['price'] is None:
return return
@@ -174,16 +242,16 @@ class Trade(_DECL_BASE):
logger.info('Updating trade (id=%d) ...', self.id) logger.info('Updating trade (id=%d) ...', self.id)
getcontext().prec = 8 # Bittrex do not go above 8 decimal getcontext().prec = 8 # Bittrex do not go above 8 decimal
if order['type'] == 'limit' and order['side'] == 'buy': if order_type == 'limit' and order['side'] == 'buy':
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = Decimal(order['price']) self.open_rate = Decimal(order['price'])
self.amount = Decimal(order['amount']) self.amount = Decimal(order['amount'])
logger.info('LIMIT_BUY has been fulfilled for %s.', self) logger.info('LIMIT_BUY has been fulfilled for %s.', self)
self.open_order_id = None self.open_order_id = None
elif order['type'] == 'limit' and order['side'] == 'sell': elif order_type == 'limit' and order['side'] == 'sell':
self.close(order['price']) self.close(order['price'])
else: else:
raise ValueError('Unknown order type: {}'.format(order['type'])) raise ValueError(f'Unknown order type: {order_type}')
cleanup() cleanup()
def close(self, rate: float) -> None: def close(self, rate: float) -> None:
@@ -254,7 +322,8 @@ class Trade(_DECL_BASE):
rate=(rate or self.close_rate), rate=(rate or self.close_rate),
fee=(fee or self.fee_close) fee=(fee or self.fee_close)
) )
return float("{0:.8f}".format(close_trade_price - open_trade_price)) profit = close_trade_price - open_trade_price
return float(f"{profit:.8f}")
def calc_profit_percent( def calc_profit_percent(
self, self,
@@ -274,5 +343,5 @@ class Trade(_DECL_BASE):
rate=(rate or self.close_rate), rate=(rate or self.close_rate),
fee=(fee or self.fee_close) fee=(fee or self.fee_close)
) )
profit_percent = (close_trade_price / open_trade_price) - 1
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1)) return float(f"{profit_percent:.8f}")

View File

@@ -0,0 +1,2 @@
from .rpc import RPC, RPCMessageType, RPCException # noqa
from .rpc_manager import RPCManager # noqa

View File

@@ -3,22 +3,37 @@ This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import datetime, timedelta, date from datetime import timedelta, datetime, date
from decimal import Decimal from decimal import Decimal
from typing import Dict, Tuple, Any, List from enum import Enum
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 from numpy import mean, nan_to_num
from pandas import DataFrame from pandas import DataFrame
from freqtrade import TemporaryError
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import SellType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RPCMessageType(Enum):
STATUS_NOTIFICATION = 'status'
WARNING_NOTIFICATION = 'warning'
CUSTOM_NOTIFICATION = 'custom'
BUY_NOTIFICATION = 'buy'
SELL_NOTIFICATION = 'sell'
def __repr__(self):
return self.value
class RPCException(Exception): class RPCException(Exception):
""" """
Should be raised with a rpc-formatted message in an _rpc_* method Should be raised with a rpc-formatted message in an _rpc_* method
@@ -26,13 +41,21 @@ class RPCException(Exception):
raise RPCException('*Status:* `no active trade`') raise RPCException('*Status:* `no active trade`')
""" """
pass def __init__(self, message: str) -> None:
super().__init__(self)
self.message = message
def __str__(self):
return self.message
class RPC(object): class RPC(object):
""" """
RPC class can be used to have extra feature, like bot data, and access to DB data RPC class can be used to have extra feature, like bot data, and access to DB data
""" """
# Bind _fiat_converter if needed in each RPC handler
_fiat_converter: Optional[CryptoToFiatConverter] = None
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """
Initializes all enabled rpc modules Initializes all enabled rpc modules
@@ -41,20 +64,20 @@ class RPC(object):
""" """
self._freqtrade = freqtrade self._freqtrade = freqtrade
@property
def name(self) -> str:
""" Returns the lowercase name of the implementation """
return self.__class__.__name__.lower()
@abstractmethod @abstractmethod
def cleanup(self) -> None: def cleanup(self) -> None:
""" Cleanup pending module resources """ """ Cleanup pending module resources """
@property
@abstractmethod @abstractmethod
def name(self) -> str: def send_msg(self, msg: Dict[str, str]) -> None:
""" Returns the lowercase name of this module """
@abstractmethod
def send_msg(self, msg: str) -> None:
""" Sends a message to all registered rpc modules """ """ Sends a message to all registered rpc modules """
def _rpc_trade_status(self) -> List[str]: def _rpc_trade_status(self) -> List[Dict[str, Any]]:
""" """
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function a remotely exposed function
@@ -62,11 +85,11 @@ class RPC(object):
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('*Status:* `trader is not running`') raise RPCException('trader is not running')
elif not trades: elif not trades:
raise RPCException('*Status:* `no active trade`') raise RPCException('no active trade')
else: else:
result = [] results = []
for trade in trades: for trade in trades:
order = None order = None
if trade.open_order_id: if trade.open_order_id:
@@ -74,53 +97,42 @@ class RPC(object):
# calculate profit and send message to user # calculate profit and send message to user
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate) current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = '{:.2f}%'.format( fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
round(trade.close_profit * 100, 2) if trade.close_profit else None)
) if trade.close_profit else None results.append(dict(
message = "*Trade ID:* `{trade_id}`\n" \ trade_id=trade.id,
"*Current Pair:* [{pair}]({market_url})\n" \ pair=trade.pair,
"*Open Since:* `{date}`\n" \ market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
"*Amount:* `{amount}`\n" \ date=arrow.get(trade.open_date),
"*Open Rate:* `{open_rate:.8f}`\n" \ open_rate=trade.open_rate,
"*Close Rate:* `{close_rate}`\n" \ close_rate=trade.close_rate,
"*Current Rate:* `{current_rate:.8f}`\n" \ current_rate=current_rate,
"*Close Profit:* `{close_profit}`\n" \ amount=round(trade.amount, 8),
"*Current Profit:* `{current_profit:.2f}%`\n" \ close_profit=fmt_close_profit,
"*Open Order:* `{open_order}`"\ current_profit=round(current_profit * 100, 2),
.format( open_order='({} {} rem={:.8f})'.format(
trade_id=trade.id, order['type'], order['side'], order['remaining']
pair=trade.pair, ) if order else None,
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair), ))
date=arrow.get(trade.open_date).humanize(), return results
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit * 100, 2),
open_order='({} {} rem={:.8f})'.format(
order['type'], order['side'], order['remaining']
) if order else None,
)
result.append(message)
return result
def _rpc_status_table(self) -> DataFrame: def _rpc_status_table(self) -> DataFrame:
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('*Status:* `trader is not running`') raise RPCException('trader is not running')
elif not trades: elif not trades:
raise RPCException('*Status:* `no active order`') raise RPCException('no active order')
else: else:
trades_list = [] trades_list = []
for trade in trades: for trade in trades:
# calculate profit and send message to user # calculate profit and send message to user
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
trade_perc = (100 * trade.calc_profit_percent(current_rate))
trades_list.append([ trades_list.append([
trade.id, trade.id,
trade.pair, trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)) f'{trade_perc:.2f}%'
]) ])
columns = ['ID', 'Pair', 'Since', 'Profit'] columns = ['ID', 'Pair', 'Since', 'Profit']
@@ -135,9 +147,8 @@ class RPC(object):
profit_days: Dict[date, Dict] = {} profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0): if not (isinstance(timescale, int) and timescale > 0):
raise RPCException('*Daily [n]:* `must be an integer greater than 0`') raise RPCException('timescale must be an integer greater than 0')
fiat = self._freqtrade.fiat_converter
for day in range(0, timescale): for day in range(0, timescale):
profitday = today - timedelta(days=day) profitday = today - timedelta(days=day)
trades = Trade.query \ trades = Trade.query \
@@ -148,7 +159,7 @@ class RPC(object):
.all() .all()
curdayprofit = sum(trade.calc_profit() for trade in trades) curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = { profit_days[profitday] = {
'amount': format(curdayprofit, '.8f'), 'amount': f'{curdayprofit:.8f}',
'trades': len(trades) 'trades': len(trades)
} }
@@ -160,11 +171,11 @@ class RPC(object):
symbol=stake_currency symbol=stake_currency
), ),
'{value:.3f} {symbol}'.format( '{value:.3f} {symbol}'.format(
value=fiat.convert_amount( value=self._fiat_converter.convert_amount(
value['amount'], value['amount'],
stake_currency, stake_currency,
fiat_display_currency fiat_display_currency
), ) if self._fiat_converter else 0,
symbol=fiat_display_currency symbol=fiat_display_currency
), ),
'{value} trade{s}'.format( '{value} trade{s}'.format(
@@ -215,34 +226,33 @@ class RPC(object):
.order_by(sql.text('profit_sum DESC')).first() .order_by(sql.text('profit_sum DESC')).first()
if not best_pair: if not best_pair:
raise RPCException('*Status:* `no closed trade`') raise RPCException('no closed trade')
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
# FIX: we want to keep fiatconverter in a state/environment,
# doing this will utilize its caching functionallity, instead we reinitialize it here
fiat = self._freqtrade.fiat_converter
# Prepare data to display # Prepare data to display
profit_closed_coin = 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(nan_to_num(mean(profit_closed_percent)) * 100, 2)
profit_closed_fiat = fiat.convert_amount( profit_closed_fiat = self._fiat_converter.convert_amount(
profit_closed_coin, profit_closed_coin_sum,
stake_currency, stake_currency,
fiat_display_currency fiat_display_currency
) ) if self._fiat_converter else 0
profit_all_coin = 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(nan_to_num(mean(profit_all_percent)) * 100, 2)
profit_all_fiat = fiat.convert_amount( profit_all_fiat = self._fiat_converter.convert_amount(
profit_all_coin, profit_all_coin_sum,
stake_currency, stake_currency,
fiat_display_currency fiat_display_currency
) ) if self._fiat_converter else 0
num = float(len(durations) or 1) num = float(len(durations) or 1)
return { return {
'profit_closed_coin': profit_closed_coin, 'profit_closed_coin': profit_closed_coin_sum,
'profit_closed_percent': profit_closed_percent, 'profit_closed_percent': profit_closed_percent,
'profit_closed_fiat': profit_closed_fiat, 'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin, 'profit_all_coin': profit_all_coin_sum,
'profit_all_percent': profit_all_percent, 'profit_all_percent': profit_all_percent,
'profit_all_fiat': profit_all_fiat, 'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades), 'trade_count': len(trades),
@@ -253,7 +263,7 @@ class RPC(object):
'best_rate': round(bp_rate * 100, 2), 'best_rate': round(bp_rate * 100, 2),
} }
def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]: def _rpc_balance(self, fiat_display_currency: str) -> Dict:
""" Returns current account balance per crypto """ """ Returns current account balance per crypto """
output = [] output = []
total = 0.0 total = 0.0
@@ -264,51 +274,56 @@ class RPC(object):
if coin == 'BTC': if coin == 'BTC':
rate = 1.0 rate = 1.0
else: else:
if coin == 'USDT': try:
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid'] if coin == 'USDT':
else: rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid'] else:
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
except TemporaryError:
continue
est_btc: float = rate * balance['total'] est_btc: float = rate * balance['total']
total = total + est_btc total = total + est_btc
output.append( output.append({
{ 'currency': coin,
'currency': coin, 'available': balance['free'],
'available': balance['free'], 'balance': balance['total'],
'balance': balance['total'], 'pending': balance['used'],
'pending': balance['used'], 'est_btc': est_btc,
'est_btc': est_btc })
}
)
if total == 0.0: if total == 0.0:
raise RPCException('`All balances are zero.`') raise RPCException('all balances are zero')
fiat = self._freqtrade.fiat_converter
symbol = fiat_display_currency symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol) value = self._fiat_converter.convert_amount(total, 'BTC',
return output, total, symbol, value symbol) if self._fiat_converter else 0
return {
'currencies': output,
'total': total,
'symbol': symbol,
'value': value,
}
def _rpc_start(self) -> str: def _rpc_start(self) -> Dict[str, str]:
""" Handler for start """ """ Handler for start """
if self._freqtrade.state == State.RUNNING: if self._freqtrade.state == State.RUNNING:
return '*Status:* `already running`' return {'status': 'already running'}
self._freqtrade.state = State.RUNNING self._freqtrade.state = State.RUNNING
return '`Starting trader ...`' return {'status': 'starting trader ...'}
def _rpc_stop(self) -> str: def _rpc_stop(self) -> Dict[str, str]:
""" Handler for stop """ """ Handler for stop """
if self._freqtrade.state == State.RUNNING: if self._freqtrade.state == State.RUNNING:
self._freqtrade.state = State.STOPPED self._freqtrade.state = State.STOPPED
return '`Stopping trader ...`' return {'status': 'stopping trader ...'}
return '*Status:* `already stopped`' return {'status': 'already stopped'}
def _rpc_reload_conf(self) -> str: def _rpc_reload_conf(self) -> Dict[str, str]:
""" Handler for reload_conf. """ """ Handler for reload_conf. """
self._freqtrade.state = State.RELOAD_CONF self._freqtrade.state = State.RELOAD_CONF
return '*Status:* `Reloading config ...`' return {'status': 'reloading config ...'}
# FIX: no test for this!!!!
def _rpc_forcesell(self, trade_id) -> None: def _rpc_forcesell(self, trade_id) -> None:
""" """
Handler for forcesell <id>. Handler for forcesell <id>.
@@ -338,11 +353,11 @@ class RPC(object):
# Get current rate and execute sell # Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
self._freqtrade.execute_sell(trade, current_rate) self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`') raise RPCException('trader is not running')
if trade_id == 'all': if trade_id == 'all':
# Execute sell for all open orders # Execute sell for all open orders
@@ -359,7 +374,7 @@ class RPC(object):
).first() ).first()
if not trade: if not trade:
logger.warning('forcesell: Invalid argument received') logger.warning('forcesell: Invalid argument received')
raise RPCException('Invalid argument.') raise RPCException('invalid argument')
_exec_forcesell(trade) _exec_forcesell(trade)
Trade.session.flush() Trade.session.flush()
@@ -370,7 +385,7 @@ class RPC(object):
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
""" """
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`') raise RPCException('trader is not running')
pair_rates = Trade.session.query(Trade.pair, pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'), sql.func.sum(Trade.close_profit).label('profit_sum'),
@@ -387,6 +402,6 @@ class RPC(object):
def _rpc_count(self) -> List[Trade]: def _rpc_count(self) -> List[Trade]:
""" Returns the number of trades running """ """ Returns the number of trades running """
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`') raise RPCException('trader is not running')
return Trade.query.filter(Trade.is_open.is_(True)).all() return Trade.query.filter(Trade.is_open.is_(True)).all()

View File

@@ -2,9 +2,9 @@
This module contains class to manage RPC communications (Telegram, Slack, ...) This module contains class to manage RPC communications (Telegram, Slack, ...)
""" """
import logging import logging
from typing import List from typing import List, Dict, Any
from freqtrade.rpc.rpc import RPC from freqtrade.rpc import RPC
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -23,6 +23,12 @@ class RPCManager(object):
from freqtrade.rpc.telegram import Telegram from freqtrade.rpc.telegram import Telegram
self.registered_modules.append(Telegram(freqtrade)) self.registered_modules.append(Telegram(freqtrade))
# Enable Webhook
if freqtrade.config.get('webhook', {}).get('enabled', False):
logger.info('Enabling rpc.webhook ...')
from freqtrade.rpc.webhook import Webhook
self.registered_modules.append(Webhook(freqtrade))
def cleanup(self) -> None: def cleanup(self) -> None:
""" Stops all enabled rpc modules """ """ Stops all enabled rpc modules """
logger.info('Cleaning up rpc modules ...') logger.info('Cleaning up rpc modules ...')
@@ -32,11 +38,14 @@ class RPCManager(object):
mod.cleanup() mod.cleanup()
del mod del mod
def send_msg(self, msg: str) -> None: def send_msg(self, msg: Dict[str, Any]) -> None:
""" """
Send given markdown message to all registered rpc modules Send given message to all registered rpc modules.
:param msg: message A message consists of one or more key value pairs of strings.
:return: None e.g.:
{
'status': 'stopping bot'
}
""" """
logger.info('Sending rpc message: %s', msg) logger.info('Sending rpc message: %s', msg)
for mod in self.registered_modules: for mod in self.registered_modules:

View File

@@ -4,7 +4,7 @@
This module manage Telegram communication This module manage Telegram communication
""" """
import logging import logging
from typing import Any, Callable from typing import Any, Callable, Dict
from tabulate import tabulate from tabulate import tabulate
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
@@ -12,7 +12,8 @@ from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.rpc import RPC, RPCException, RPCMessageType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -55,10 +56,6 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
class Telegram(RPC): class Telegram(RPC):
""" This class handles all telegram communication """ """ This class handles all telegram communication """
@property
def name(self) -> str:
return "telegram"
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """
Init the Telegram call, and init the super class RPC Init the Telegram call, and init the super class RPC
@@ -70,6 +67,8 @@ class Telegram(RPC):
self._updater: Updater = None self._updater: Updater = None
self._config = freqtrade.config self._config = freqtrade.config
self._init() self._init()
if self._config.get('fiat_display_currency', None):
self._fiat_converter = CryptoToFiatConverter()
def _init(self) -> None: def _init(self) -> None:
""" """
@@ -114,9 +113,57 @@ class Telegram(RPC):
""" """
self._updater.stop() self._updater.stop()
def send_msg(self, msg: str) -> None: def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """ """ Send a message to telegram channel """
self._send_msg(msg)
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
if self._fiat_converter:
msg['stake_amount_fiat'] = self._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else:
msg['stake_amount_fiat'] = 0
message = "*{exchange}:* Buying [{pair}]({market_url})\n" \
"with limit `{limit:.8f}\n" \
"({stake_amount:.6f} {stake_currency}".format(**msg)
if msg.get('fiat_currency', None):
message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
message += ")`"
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
msg['amount'] = round(msg['amount'], 8)
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
message = "*{exchange}:* Selling [{pair}]({market_url})\n" \
"*Limit:* `{limit:.8f}`\n" \
"*Amount:* `{amount:.8f}`\n" \
"*Open Rate:* `{open_rate:.8f}`\n" \
"*Current Rate:* `{current_rate:.8f}`\n" \
"*Profit:* `{profit_percent:.2f}%`".format(**msg)
# Check if all sell properties are available.
# This might not be the case if the message origin is triggered by /forcesell
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
and self._fiat_converter):
msg['profit_fiat'] = self._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
message += '` ({gain}: {profit_amount:.8f} {stake_currency}`' \
'` / {profit_fiat:.3f} {fiat_currency})`'.format(**msg)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
message = '*Status:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
message = '*Warning:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
message = '{status}'.format(**msg)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
self._send_msg(message)
@authorized_only @authorized_only
def _status(self, bot: Bot, update: Update) -> None: def _status(self, bot: Bot, update: Update) -> None:
@@ -136,8 +183,26 @@ class Telegram(RPC):
return return
try: try:
for trade_msg in self._rpc_trade_status(): results = self._rpc_trade_status()
self._send_msg(trade_msg, bot=bot) # pre format data
for result in results:
result['date'] = result['date'].humanize()
messages = [
"*Trade ID:* `{trade_id}`\n"
"*Current Pair:* [{pair}]({market_url})\n"
"*Open Since:* `{date}`\n"
"*Amount:* `{amount}`\n"
"*Open Rate:* `{open_rate:.8f}`\n"
"*Close Rate:* `{close_rate}`\n"
"*Current Rate:* `{current_rate:.8f}`\n"
"*Close Profit:* `{close_profit}`\n"
"*Current Profit:* `{current_profit:.2f}%`\n"
"*Open Order:* `{open_order}`".format(**result)
for result in results
]
for msg in messages:
self._send_msg(msg, bot=bot)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e), bot=bot)
@@ -153,7 +218,7 @@ class Telegram(RPC):
try: try:
df_statuses = self._rpc_status_table() df_statuses = self._rpc_status_table()
message = tabulate(df_statuses, headers='keys', tablefmt='simple') message = tabulate(df_statuses, headers='keys', tablefmt='simple')
self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML) self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e), bot=bot)
@@ -166,6 +231,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
try: try:
timescale = int(update.message.text.replace('/daily', '').strip()) timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -173,18 +240,17 @@ class Telegram(RPC):
try: try:
stats = self._rpc_daily_profit( stats = self._rpc_daily_profit(
timescale, timescale,
self._config['stake_currency'], stake_cur,
self._config['fiat_display_currency'] fiat_disp_cur
) )
stats = tabulate(stats, stats = tabulate(stats,
headers=[ headers=[
'Day', 'Day',
'Profit {}'.format(self._config['stake_currency']), f'Profit {stake_cur}',
'Profit {}'.format(self._config['fiat_display_currency']) f'Profit {fiat_disp_cur}'
], ],
tablefmt='simple') tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\ message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats}</pre>'
.format(timescale, stats)
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e), bot=bot)
@@ -198,39 +264,38 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '')
try: try:
stats = self._rpc_trade_statistics( stats = self._rpc_trade_statistics(
self._config['stake_currency'], stake_cur,
self._config['fiat_display_currency']) fiat_disp_cur)
profit_closed_coin = stats['profit_closed_coin']
profit_closed_percent = stats['profit_closed_percent']
profit_closed_fiat = stats['profit_closed_fiat']
profit_all_coin = stats['profit_all_coin']
profit_all_percent = stats['profit_all_percent']
profit_all_fiat = stats['profit_all_fiat']
trade_count = stats['trade_count']
first_trade_date = stats['first_trade_date']
latest_trade_date = stats['latest_trade_date']
avg_duration = stats['avg_duration']
best_pair = stats['best_pair']
best_rate = stats['best_rate']
# Message to display # Message to display
markdown_msg = "*ROI:* Close trades\n" \ markdown_msg = "*ROI:* Close trades\n" \
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \ f"∙ `{profit_closed_coin:.8f} {stake_cur} "\
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \ f"({profit_closed_percent:.2f}%)`\n" \
"*ROI:* All trades\n" \ f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n" \
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \ f"*ROI:* All trades\n" \
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \ f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" \
"*Total Trade Count:* `{trade_count}`\n" \ f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" \
"*First Trade opened:* `{first_trade_date}`\n" \ f"*Total Trade Count:* `{trade_count}`\n" \
"*Latest Trade opened:* `{latest_trade_date}`\n" \ f"*First Trade opened:* `{first_trade_date}`\n" \
"*Avg. Duration:* `{avg_duration}`\n" \ f"*Latest Trade opened:* `{latest_trade_date}`\n" \
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\ f"*Avg. Duration:* `{avg_duration}`\n" \
.format( f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
coin=self._config['stake_currency'],
fiat=self._config['fiat_display_currency'],
profit_closed_coin=stats['profit_closed_coin'],
profit_closed_percent=stats['profit_closed_percent'],
profit_closed_fiat=stats['profit_closed_fiat'],
profit_all_coin=stats['profit_all_coin'],
profit_all_percent=stats['profit_all_percent'],
profit_all_fiat=stats['profit_all_fiat'],
trade_count=stats['trade_count'],
first_trade_date=stats['first_trade_date'],
latest_trade_date=stats['latest_trade_date'],
avg_duration=stats['avg_duration'],
best_pair=stats['best_pair'],
best_rate=stats['best_rate']
)
self._send_msg(markdown_msg, bot=bot) self._send_msg(markdown_msg, bot=bot)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e), bot=bot)
@@ -239,10 +304,9 @@ class Telegram(RPC):
def _balance(self, bot: Bot, update: Update) -> None: def _balance(self, bot: Bot, update: Update) -> None:
""" Handler for /balance """ """ Handler for /balance """
try: try:
currencys, total, symbol, value = \ result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
self._rpc_balance(self._config['fiat_display_currency'])
output = '' output = ''
for currency in currencys: for currency in result['currencies']:
output += "*{currency}:*\n" \ output += "*{currency}:*\n" \
"\t`Available: {available: .8f}`\n" \ "\t`Available: {available: .8f}`\n" \
"\t`Balance: {balance: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \
@@ -250,8 +314,8 @@ class Telegram(RPC):
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
output += "\n*Estimated Value*:\n" \ output += "\n*Estimated Value*:\n" \
"\t`BTC: {0: .8f}`\n" \ "\t`BTC: {total: .8f}`\n" \
"\t`{1}: {2: .2f}`\n".format(total, symbol, value) "\t`{symbol}: {value: .2f}`\n".format(**result)
self._send_msg(output, bot=bot) self._send_msg(output, bot=bot)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e), bot=bot)
@@ -266,7 +330,7 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_start() msg = self._rpc_start()
self._send_msg(msg, bot=bot) self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
@authorized_only @authorized_only
def _stop(self, bot: Bot, update: Update) -> None: def _stop(self, bot: Bot, update: Update) -> None:
@@ -278,7 +342,7 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_stop() msg = self._rpc_stop()
self._send_msg(msg, bot=bot) self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
@authorized_only @authorized_only
def _reload_conf(self, bot: Bot, update: Update) -> None: def _reload_conf(self, bot: Bot, update: Update) -> None:
@@ -290,7 +354,7 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_reload_conf() msg = self._rpc_reload_conf()
self._send_msg(msg, bot=bot) self._send_msg('Status: `{status}`'.format(**msg), bot=bot)
@authorized_only @authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None: def _forcesell(self, bot: Bot, update: Update) -> None:

66
freqtrade/rpc/webhook.py Normal file
View File

@@ -0,0 +1,66 @@
"""
This module manages webhook communication
"""
import logging
from typing import Any, Dict
from requests import post, RequestException
from freqtrade.rpc import RPC, RPCMessageType
logger = logging.getLogger(__name__)
logger.debug('Included module rpc.webhook ...')
class Webhook(RPC):
""" This class handles all webhook communication """
def __init__(self, freqtrade) -> None:
"""
Init the Webhook class, and init the super class RPC
:param freqtrade: Instance of a freqtrade bot
:return: None
"""
super().__init__(freqtrade)
self._config = freqtrade.config
self._url = self._config['webhook']['url']
def cleanup(self) -> None:
"""
Cleanup pending module resources.
This will do nothing for webhooks, they will simply not be called anymore
"""
pass
def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """
try:
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
valuedict = self._config['webhook'].get('webhookbuy', None)
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhooksell', None)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
valuedict = self._config['webhook'].get('webhookstatus', None)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
if not valuedict:
logger.info("Message type %s not configured for webhooks", msg['type'])
return
payload = {key: value.format(**msg) for (key, value) in valuedict.items()}
self._send_msg(payload)
except KeyError as exc:
logger.exception("Problem calling Webhook. Please check your webhook configuration. "
"Exception: %s", exc)
def _send_msg(self, payload: dict) -> None:
"""do the actual call to the webhook"""
try:
post(self._url, data=payload)
except RequestException as exc:
logger.warning("Could not call webhook url. Exception: %s", exc)

View File

@@ -1,19 +1,31 @@
import logging import logging
import sys
from copy import deepcopy from copy import deepcopy
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
# Import Default-Strategy to have hyperopt correctly resolve
from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def import_strategy(strategy: IStrategy) -> IStrategy: def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
""" """
Imports given Strategy instance to global scope Imports given Strategy instance to global scope
of freqtrade.strategy and returns an instance of it of freqtrade.strategy and returns an instance of it
""" """
# Copy all attributes from base class and class # Copy all attributes from base class and class
attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__})
comb = {**strategy.__class__.__dict__, **strategy.__dict__}
# Delete '_abc_impl' from dict as deepcopy fails on 3.7 with
# `TypeError: can't pickle _abc_data objects``
# This will only apply to python 3.7
if sys.version_info.major == 3 and sys.version_info.minor == 7 and '_abc_impl' in comb:
del comb['_abc_impl']
attr = deepcopy(comb)
# Adjust module name # Adjust module name
attr['__module__'] = 'freqtrade.strategy' attr['__module__'] = 'freqtrade.strategy'
@@ -29,4 +41,4 @@ def import_strategy(strategy: IStrategy) -> IStrategy:
# Modify global scope to declare class # Modify global scope to declare class
globals()[name] = clazz globals()[name] = clazz
return clazz() return clazz(config)

View File

@@ -28,13 +28,16 @@ class DefaultStrategy(IStrategy):
# Optimal ticker interval for the strategy # Optimal ticker interval for the strategy
ticker_interval = '5m' ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
""" """
# Momentum Indicator # Momentum Indicator
@@ -196,10 +199,11 @@ class DefaultStrategy(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
@@ -217,10 +221,11 @@ class DefaultStrategy(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[

View File

@@ -2,11 +2,50 @@
IStrategy interface IStrategy interface
This module defines the interface to apply for strategies This module defines the interface to apply for strategies
""" """
from typing import Dict import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime
from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Tuple
import warnings
import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade import constants
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
class SignalType(Enum):
"""
Enum to distinguish between buy and sell signals
"""
BUY = "buy"
SELL = "sell"
class SellType(Enum):
"""
Enum to distinguish between sell reasons
"""
ROI = "roi"
STOP_LOSS = "stop_loss"
TRAILING_STOP_LOSS = "trailing_stop_loss"
SELL_SIGNAL = "sell_signal"
FORCE_SELL = "force_sell"
NONE = ""
class SellCheckTuple(NamedTuple):
"""
NamedTuple for Sell type + reason
"""
sell_flag: bool
sell_type: SellType
class IStrategy(ABC): class IStrategy(ABC):
""" """
@@ -19,30 +58,296 @@ class IStrategy(ABC):
ticker_interval -> str: value of the ticker interval to use for the strategy ticker_interval -> str: value of the ticker interval to use for the strategy
""" """
_populate_fun_len: int = 0
_buy_fun_len: int = 0
_sell_fun_len: int = 0
# associated minimal roi
minimal_roi: Dict minimal_roi: Dict
# associated stoploss
stoploss: float stoploss: float
# associated ticker interval
ticker_interval: str ticker_interval: str
# run "populate_indicators" only for new candle
process_only_new_candles: bool = False
# Dict to determine if analysis is necessary
_last_candle_seen_per_pair: Dict[str, datetime] = {}
def __init__(self, config: dict) -> None:
self.config = config
self._last_candle_seen_per_pair = {}
@abstractmethod @abstractmethod
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: def populate_indicators(self, 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()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """
@abstractmethod @abstractmethod
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
@abstractmethod @abstractmethod
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with sell column :return: DataFrame with sell column
""" """
def get_strategy_name(self) -> str:
"""
Returns strategy class name
"""
return self.__class__.__name__
def analyze_ticker(self, ticker_history: List[Dict], metadata: dict) -> DataFrame:
"""
Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
"""
dataframe = parse_ticker_dataframe(ticker_history)
pair = str(metadata.get('pair'))
# Test if seen this pair and last candle before.
# always run if process_only_new_candles is set to true
if (not self.process_only_new_candles or
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
# Defs that only make change on new candle data.
logging.debug("TA Analysis Launched")
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']
else:
logging.debug("Skippinig TA Analysis for already analyzed candle")
dataframe['buy'] = 0
dataframe['sell'] = 0
# Other Defs in strategy that want to be called every loop here
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
logging.debug("Loop Analysis Launched")
return dataframe
def get_signal(self, pair: str, interval: str,
ticker_hist: Optional[List[Dict]]) -> Tuple[bool, bool]:
"""
Calculates current signal based several technical analysis indicators
:param pair: pair in format ANT/BTC
:param interval: Interval to use (in min)
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
"""
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return False, False
try:
dataframe = self.analyze_ticker(ticker_hist, {'pair': pair})
except ValueError as error:
logger.warning(
'Unable to analyze ticker for pair %s: %s',
pair,
str(error)
)
return False, False
except Exception as error:
logger.exception(
'Unexpected error when analyzing ticker for pair %s: %s',
pair,
str(error)
)
return False, False
if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair)
return False, False
latest = dataframe.iloc[-1]
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
offset = self.config.get('exchange', {}).get('outdated_offset', 5)
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))):
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,
(arrow.utcnow() - signal_date).seconds // 60
)
return False, False
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
logger.debug(
'trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'],
pair,
str(buy),
str(sell)
)
return buy, sell
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
sell: bool) -> SellCheckTuple:
"""
This function evaluate if on the condition required to trigger a sell has been reached
if the threshold is reached and updates the trade record.
:return: True if trade should be sold, False otherwise
"""
current_profit = trade.calc_profit_percent(rate)
stoplossflag = self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date,
current_profit=current_profit)
if stoplossflag.sell_flag:
return stoplossflag
experimental = self.config.get('experimental', {})
if buy and experimental.get('ignore_roi_if_buy_signal', False):
logger.debug('Buy signal still active - not selling.')
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
logger.debug('Required profit reached. Selling..')
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
if experimental.get('sell_profit_only', False):
logger.debug('Checking if trade is profitable..')
if trade.calc_profit(rate=rate) <= 0:
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
if sell and not buy and experimental.get('use_sell_signal', False):
logger.debug('Sell signal received. Selling..')
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime,
current_profit: float) -> SellCheckTuple:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not
:param current_profit: current profit in percent
"""
trailing_stop = self.config.get('trailing_stop', False)
trade.adjust_stop_loss(trade.open_rate, self.stoploss, initial=True)
# evaluate if the stoploss was hit
if self.stoploss is not None and trade.stop_loss >= current_rate:
selltype = SellType.STOP_LOSS
if trailing_stop:
selltype = SellType.TRAILING_STOP_LOSS
logger.debug(
f"HIT STOP: current price at {current_rate:.6f}, "
f"stop loss is {trade.stop_loss:.6f}, "
f"initial stop loss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}")
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}")
logger.debug('Stop loss hit.')
return SellCheckTuple(sell_flag=True, sell_type=selltype)
# update the stop loss afterwards, after all by definition it's supposed to be hanging
if trailing_stop:
# check if we have a special stop loss for positive condition
# and if profit is positive
stop_loss_value = self.stoploss
sl_offset = self.config.get('trailing_stop_positive_offset', 0.0)
if 'trailing_stop_positive' in self.config and current_profit > sl_offset:
# Ignore mypy error check in configuration that this is a float
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
f"with offset {sl_offset:.4g} "
f"since we have profit {current_profit:.4f}%")
trade.adjust_stop_loss(current_rate, stop_loss_value)
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
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
sell
:return True if bot should sell at current rate
"""
# Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
for duration, threshold in self.minimal_roi.items():
if time_diff <= duration:
return False
if current_profit > threshold:
return True
return False
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""
Creates a dataframe and populates indicators for given ticker data
"""
return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), {'pair': pair})
for pair, pair_data in tickerdata.items()}
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Populate indicators that will be used in the Buy and Sell strategy
This method should not be overridden.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
if self._populate_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_indicators(dataframe) # type: ignore
else:
return self.populate_indicators(dataframe, metadata)
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
if self._buy_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_buy_trend(dataframe) # type: ignore
else:
return self.populate_buy_trend(dataframe, metadata)
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param pair: Additional information, like the currently traded pair
:return: DataFrame with sell column
"""
if self._sell_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_sell_trend(dataframe) # type: ignore
else:
return self.populate_sell_trend(dataframe, metadata)

View File

@@ -7,14 +7,16 @@ import importlib.util
import inspect import inspect
import logging import logging
import os import os
import tempfile
from base64 import urlsafe_b64decode
from collections import OrderedDict from collections import OrderedDict
from typing import Optional, Dict, Type from pathlib import Path
from typing import Dict, Optional, Type
from freqtrade import constants from freqtrade import constants
from freqtrade.strategy import import_strategy from freqtrade.strategy import import_strategy
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -35,26 +37,43 @@ class StrategyResolver(object):
# Verify the strategy is in the configuration, otherwise fallback to the default strategy # Verify the strategy is in the configuration, otherwise fallback to the default strategy
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
self.strategy: IStrategy = self._load_strategy(strategy_name, self.strategy: IStrategy = self._load_strategy(strategy_name,
config=config,
extra_dir=config.get('strategy_path')) extra_dir=config.get('strategy_path'))
# Set attributes # Set attributes
# Check if we need to override configuration # Check if we need to override configuration
if 'minimal_roi' in config: if 'minimal_roi' in config:
self.strategy.minimal_roi = config['minimal_roi'] self.strategy.minimal_roi = config['minimal_roi']
logger.info("Override strategy \'minimal_roi\' with value in config file.") logger.info("Override strategy 'minimal_roi' with value in config file: %s.",
config['minimal_roi'])
else:
config['minimal_roi'] = self.strategy.minimal_roi
if 'stoploss' in config: if 'stoploss' in config:
self.strategy.stoploss = config['stoploss'] self.strategy.stoploss = config['stoploss']
logger.info( logger.info(
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss'] "Override strategy 'stoploss' with value in config file: %s.", config['stoploss']
) )
else:
config['stoploss'] = self.strategy.stoploss
if 'ticker_interval' in config: if 'ticker_interval' in config:
self.strategy.ticker_interval = config['ticker_interval'] self.strategy.ticker_interval = config['ticker_interval']
logger.info( logger.info(
"Override strategy \'ticker_interval\' with value in config file: %s.", "Override strategy 'ticker_interval' with value in config file: %s.",
config['ticker_interval'] config['ticker_interval']
) )
else:
config['ticker_interval'] = self.strategy.ticker_interval
if 'process_only_new_candles' in config:
self.strategy.process_only_new_candles = config['process_only_new_candles']
logger.info(
"Override process_only_new_candles 'process_only_new_candles' "
"with value in config file: %s.", config['process_only_new_candles']
)
else:
config['process_only_new_candles'] = self.strategy.process_only_new_candles
# Sort and apply type conversions # Sort and apply type conversions
self.strategy.minimal_roi = OrderedDict(sorted( self.strategy.minimal_roi = OrderedDict(sorted(
@@ -63,10 +82,11 @@ class StrategyResolver(object):
self.strategy.stoploss = float(self.strategy.stoploss) self.strategy.stoploss = float(self.strategy.stoploss)
def _load_strategy( def _load_strategy(
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy: self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy:
""" """
Search and loads the specified strategy. Search and loads the specified strategy.
:param strategy_name: name of the module to import :param strategy_name: name of the module to import
:param config: configuration for the strategy
:param extra_dir: additional directory to search for the given strategy :param extra_dir: additional directory to search for the given strategy
:return: Strategy instance or None :return: Strategy instance or None
""" """
@@ -80,12 +100,35 @@ class StrategyResolver(object):
# Add extra strategy directory on top of search paths # Add extra strategy directory on top of search paths
abs_paths.insert(0, extra_dir) abs_paths.insert(0, extra_dir)
if ":" in strategy_name:
logger.info("loading base64 endocded strategy")
strat = strategy_name.split(":")
if len(strat) == 2:
temp = Path(tempfile.mkdtemp("freq", "strategy"))
name = strat[0] + ".py"
temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8'))
temp.joinpath("__init__.py").touch()
strategy_name = os.path.splitext(name)[0]
# register temp path with the bot
abs_paths.insert(0, str(temp.resolve()))
for path in abs_paths: for path in abs_paths:
try: try:
strategy = self._search_strategy(path, strategy_name) strategy = self._search_strategy(path, strategy_name=strategy_name, config=config)
if strategy: if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
return import_strategy(strategy) strategy._populate_fun_len = len(
inspect.getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(
inspect.getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(
inspect.getfullargspec(strategy.populate_sell_trend).args)
return import_strategy(strategy, config=config)
except FileNotFoundError: except FileNotFoundError:
logger.warning('Path "%s" does not exist', path) logger.warning('Path "%s" does not exist', path)
@@ -115,7 +158,7 @@ class StrategyResolver(object):
return next(valid_strategies_gen, None) return next(valid_strategies_gen, None)
@staticmethod @staticmethod
def _search_strategy(directory: str, strategy_name: str) -> Optional[IStrategy]: def _search_strategy(directory: str, strategy_name: str, config: dict) -> Optional[IStrategy]:
""" """
Search for the strategy_name in the given directory Search for the strategy_name in the given directory
:param directory: relative or absolute directory path :param directory: relative or absolute directory path
@@ -131,5 +174,5 @@ class StrategyResolver(object):
os.path.abspath(os.path.join(directory, entry)), strategy_name os.path.abspath(os.path.join(directory, entry)), strategy_name
) )
if strategy: if strategy:
return strategy() return strategy(config)
return None return None

View File

@@ -2,17 +2,15 @@
import json import json
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Dict, Optional
from functools import reduce from functools import reduce
from unittest.mock import MagicMock from typing import Dict, Optional
from unittest.mock import MagicMock, PropertyMock
import arrow import arrow
import pytest import pytest
from jsonschema import validate
from telegram import Chat, Message, Update from telegram import Chat, Message, Update
from freqtrade.analyze import Analyze from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
from freqtrade import constants
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
@@ -20,7 +18,7 @@ logging.getLogger('').setLevel(logging.INFO)
def log_has(line, logs): def log_has(line, logs):
# caplog mocker returns log as a tuple: ('freqtrade.analyze', 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),
@@ -28,7 +26,10 @@ def log_has(line, logs):
def patch_exchange(mocker, api_mock=None) -> None: def patch_exchange(mocker, api_mock=None) -> None:
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value="Bittrex"))
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value="bittrex"))
if api_mock: if api_mock:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
else: else:
@@ -51,13 +52,11 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
""" """
# mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0}) # mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
patch_coinmarketcap(mocker, {'price_usd': 12345.0}) patch_coinmarketcap(mocker, {'price_usd': 12345.0})
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
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())
patch_exchange(mocker, None) patch_exchange(mocker, None)
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
return FreqtradeBot(config) return FreqtradeBot(config)
@@ -100,9 +99,23 @@ def default_conf():
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.10, "stoploss": -0.10,
"unfilledtimeout": 600, "unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0,
"use_order_book": False,
"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": 1
}, },
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
@@ -125,7 +138,6 @@ def default_conf():
"db_url": "sqlite://", "db_url": "sqlite://",
"loglevel": logging.DEBUG, "loglevel": logging.DEBUG,
} }
validate(configuration, constants.CONF_SCHEMA)
return configuration return configuration
@@ -404,6 +416,39 @@ def limit_sell_order():
} }
@pytest.fixture
def order_book_l2():
return MagicMock(return_value={
'bids': [
[0.043936, 10.442],
[0.043935, 31.865],
[0.043933, 11.212],
[0.043928, 0.088],
[0.043925, 10.0],
[0.043921, 10.0],
[0.04392, 37.64],
[0.043899, 0.066],
[0.043885, 0.676],
[0.04387, 22.758]
],
'asks': [
[0.043949, 0.346],
[0.04395, 0.608],
[0.043951, 3.948],
[0.043954, 0.288],
[0.043958, 9.277],
[0.043995, 1.566],
[0.044, 0.588],
[0.044002, 0.992],
[0.044003, 0.095],
[0.04402, 37.64]
],
'timestamp': None,
'datetime': None,
'nonce': 288004540
})
@pytest.fixture @pytest.fixture
def ticker_history(): def ticker_history():
return [ return [
@@ -613,7 +658,7 @@ def tickers():
@pytest.fixture @pytest.fixture
def result(): def result():
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file: with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
return Analyze.parse_ticker_dataframe(json.load(data_file)) return parse_ticker_dataframe(json.load(data_file))
# FIX: # FIX:
# Create an fixture/function # Create an fixture/function

View File

@@ -1,17 +1,53 @@
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement # pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
# pragma pylint: disable=protected-access # pragma pylint: disable=protected-access
import logging import logging
from copy import deepcopy
from random import randint
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock, PropertyMock from random import randint
from unittest.mock import Mock, MagicMock, PropertyMock
import arrow
import ccxt import ccxt
import pytest import pytest
from freqtrade import OperationalException, DependencyException, TemporaryError from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.exchange import Exchange, API_RETRY_COUNT from freqtrade.exchange import API_RETRY_COUNT, Exchange
from freqtrade.tests.conftest import log_has, get_patched_exchange from freqtrade.tests.conftest import get_patched_exchange, log_has
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
def get_mock_coro(return_value):
async def mock_coro(*args, **kwargs):
return return_value
return Mock(wraps=mock_coro)
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
def test_init(default_conf, mocker, caplog): def test_init(default_conf, mocker, caplog):
@@ -20,7 +56,13 @@ def test_init(default_conf, mocker, caplog):
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples) assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
def test_init_exception(default_conf): def test_destroy(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG)
get_patched_exchange(mocker, default_conf)
assert log_has('Exchange object destroyed, closing async loop', caplog.record_tuples)
def test_init_exception(default_conf, mocker):
default_conf['exchange']['name'] = 'wrong_exchange_name' default_conf['exchange']['name'] = 'wrong_exchange_name'
with pytest.raises( with pytest.raises(
@@ -28,6 +70,141 @@ def test_init_exception(default_conf):
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])): match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
Exchange(default_conf) Exchange(default_conf)
default_conf['exchange']['name'] = 'binance'
with pytest.raises(
OperationalException,
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError))
Exchange(default_conf)
def test_symbol_amount_prec(default_conf, mocker):
'''
Test rounds down to 4 Decimal places
'''
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance'))
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}})
type(api_mock).markets = markets
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 = Exchange(default_conf)
amount = 2.34559
pair = 'ETH/BTC'
amount = exchange.symbol_amount_prec(pair, amount)
assert amount == 2.3455
def test_symbol_price_prec(default_conf, mocker):
'''
Test rounds up to 4 decimal places
'''
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance'))
markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}})
type(api_mock).markets = markets
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 = Exchange(default_conf)
price = 2.34559
pair = 'ETH/BTC'
price = exchange.symbol_price_prec(pair, price)
assert price == 2.3456
def test_set_sandbox(default_conf, mocker):
"""
Test working scenario
"""
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com",
'api': 'https://api.gdax.com'})
type(api_mock).urls = url_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._load_async_markets', MagicMock())
exchange = Exchange(default_conf)
liveurl = exchange._api.urls['api']
default_conf['exchange']['sandbox'] = True
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
assert exchange._api.urls['api'] != liveurl
def test_set_sandbox_exception(default_conf, mocker):
"""
Test Fail scenario
"""
api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'})
type(api_mock).urls = url_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._load_async_markets', MagicMock())
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'):
exchange = Exchange(default_conf)
default_conf['exchange']['sandbox'] = True
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
def test__load_async_markets(default_conf, mocker, caplog):
exchange = get_patched_exchange(mocker, default_conf)
exchange._api_async.load_markets = get_mock_coro(None)
exchange._load_async_markets()
assert exchange._api_async.load_markets.call_count == 1
caplog.set_level(logging.DEBUG)
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
exchange._load_async_markets()
assert log_has('Could not load async markets. Reason: deadbeef',
caplog.record_tuples)
def test__load_markets(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance'))
api_mock.load_markets = MagicMock(return_value={})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
expected_return = {'ETH/BTC': 'available'}
api_mock.load_markets = MagicMock(return_value=expected_return)
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
default_conf['exchange']['pair_whitelist'] = ['ETH/BTC']
ex = Exchange(default_conf)
assert ex.markets == expected_return
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
Exchange(default_conf)
assert log_has('Unable to initialize markets. Reason: ', caplog.record_tuples)
def test_validate_pairs(default_conf, mocker): def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
@@ -38,13 +215,17 @@ def test_validate_pairs(default_conf, mocker):
type(api_mock).id = id_mock type(api_mock).id = id_mock
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._load_async_markets', MagicMock())
Exchange(default_conf) Exchange(default_conf)
def test_validate_pairs_not_available(default_conf, mocker): def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_markets = MagicMock(return_value={}) api_mock.load_markets = MagicMock(return_value={'XRP/BTC': 'inactive'})
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._load_async_markets', MagicMock())
with pytest.raises(OperationalException, match=r'not available'): with pytest.raises(OperationalException, match=r'not available'):
Exchange(default_conf) Exchange(default_conf)
@@ -55,12 +236,12 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
api_mock.load_markets = MagicMock(return_value={ api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': '' 'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
}) })
conf = deepcopy(default_conf) default_conf['stake_currency'] = 'ETH'
conf['stake_currency'] = 'ETH'
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._load_async_markets', MagicMock())
with pytest.raises(OperationalException, match=r'not compatible'): with pytest.raises(OperationalException, match=r'not compatible'):
Exchange(conf) Exchange(default_conf)
def test_validate_pairs_exception(default_conf, mocker, caplog): def test_validate_pairs_exception(default_conf, mocker, caplog):
@@ -70,31 +251,95 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
api_mock.load_markets = MagicMock(return_value={}) api_mock.load_markets = MagicMock(return_value={})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'): with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
Exchange(default_conf) Exchange(default_conf)
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError()) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
Exchange(default_conf) Exchange(default_conf)
assert log_has('Unable to validate pairs (assuming they are correct). Reason: ', assert log_has('Unable to validate pairs (assuming they are correct).',
caplog.record_tuples) caplog.record_tuples)
def test_validate_pairs_stake_exception(default_conf, mocker, caplog): def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
conf = deepcopy(default_conf) default_conf['stake_currency'] = 'ETH'
conf['stake_currency'] = 'ETH'
api_mock = MagicMock() api_mock = MagicMock()
api_mock.name = MagicMock(return_value='binance') api_mock.name = MagicMock(return_value='binance')
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
match=r'Pair ETH/BTC not compatible with stake_currency: ETH' match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
): ):
Exchange(conf) Exchange(default_conf)
def test_validate_timeframes(default_conf, mocker):
default_conf["ticker_interval"] = "5m"
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
timeframes = PropertyMock(return_value={'1m': '1m',
'5m': '5m',
'15m': '15m',
'1h': '1h'})
type(api_mock).timeframes = timeframes
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
Exchange(default_conf)
def test_validate_timeframes_failed(default_conf, mocker):
default_conf["ticker_interval"] = "3m"
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
timeframes = PropertyMock(return_value={'1m': '1m',
'5m': '5m',
'15m': '15m',
'1h': '1h'})
type(api_mock).timeframes = timeframes
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
with pytest.raises(OperationalException, match=r'Invalid ticker 3m, this Exchange supports.*'):
Exchange(default_conf)
def test_validate_timeframes_not_in_config(default_conf, mocker):
del default_conf["ticker_interval"]
api_mock = MagicMock()
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
timeframes = PropertyMock(return_value={'1m': '1m',
'5m': '5m',
'15m': '15m',
'1h': '1h'})
type(api_mock).timeframes = timeframes
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
Exchange(default_conf)
def test_exchange_has(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf)
assert not exchange.exchange_has('ASDFASDF')
api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={'deadbeef': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.exchange_has("deadbeef")
type(api_mock).has = PropertyMock(return_value={'deadbeef': False})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert not exchange.exchange_has("deadbeef")
def test_buy_dry_run(default_conf, mocker): def test_buy_dry_run(default_conf, mocker):
@@ -216,6 +461,11 @@ def test_get_balance_prod(default_conf, mocker):
exchange.get_balance(currency='BTC') exchange.get_balance(currency='BTC')
with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'):
exchange = get_patched_exchange(mocker, default_conf, api_mock)
mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={}))
exchange.get_balance(currency='BTC')
def test_get_balances_dry_run(default_conf, mocker): def test_get_balances_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
@@ -243,17 +493,8 @@ def test_get_balances_prod(default_conf, mocker):
assert exchange.get_balances()['1ST']['total'] == 10.0 assert exchange.get_balances()['1ST']['total'] == 10.0
assert exchange.get_balances()['1ST']['used'] == 0.0 assert exchange.get_balances()['1ST']['used'] == 0.0
with pytest.raises(TemporaryError): ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError) "get_balances", "fetch_balance")
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_balances()
assert api_mock.fetch_balance.call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_balances()
assert api_mock.fetch_balance.call_count == 1
def test_get_tickers(default_conf, mocker): def test_get_tickers(default_conf, mocker):
@@ -282,15 +523,8 @@ def test_get_tickers(default_conf, mocker):
assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['bid'] == 0.6
assert tickers['BCH/BTC']['ask'] == 0.5 assert tickers['BCH/BTC']['ask'] == 0.5
with pytest.raises(TemporaryError): # test retrier ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NetworkError) "get_tickers", "fetch_tickers")
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_tickers()
with pytest.raises(OperationalException):
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_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)
@@ -345,21 +579,198 @@ def test_get_ticker(default_conf, mocker):
exchange.get_ticker(pair='ETH/BTC', refresh=False) exchange.get_ticker(pair='ETH/BTC', refresh=False)
assert api_mock.fetch_ticker.call_count == 0 assert api_mock.fetch_ticker.call_count == 0
with pytest.raises(TemporaryError): # test retrier ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError) "get_ticker", "fetch_ticker",
exchange = get_patched_exchange(mocker, default_conf, api_mock) pair='ETH/BTC', refresh=True)
exchange.get_ticker(pair='ETH/BTC', refresh=True)
with pytest.raises(OperationalException):
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_ticker(pair='ETH/BTC', refresh=True)
api_mock.fetch_ticker = MagicMock(return_value={}) api_mock.fetch_ticker = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_ticker(pair='ETH/BTC', refresh=True) exchange.get_ticker(pair='ETH/BTC', refresh=True)
def test_get_history(default_conf, mocker, caplog):
exchange = get_patched_exchange(mocker, default_conf)
tick = [
[
arrow.utcnow().timestamp * 1000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
pair = 'ETH/BTC'
async def mock_candle_hist(pair, tick_interval, since_ms):
return pair, tick
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * 500 * 1.8
print(f"since = {since}")
ret = exchange.get_history(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above tick
assert len(ret) == 2
def test_refresh_tickers(mocker, default_conf, caplog) -> None:
tick = [
[
1511686200000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf)
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
pairs = ['IOTA/ETH', 'XRP/ETH']
# empty dicts
assert not exchange.klines
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m')
assert log_has(f'Refreshing klines for {len(pairs)} pairs', caplog.record_tuples)
assert exchange.klines
for pair in pairs:
assert exchange.klines[pair]
@pytest.mark.asyncio
async def test__async_get_candle_history(default_conf, mocker, caplog):
tick = [
[
arrow.utcnow().timestamp * 1000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf)
# Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
exchange = Exchange(default_conf)
pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m")
assert type(res) is tuple
assert len(res) == 2
assert res[0] == pair
assert res[1] == tick
assert exchange._api_async.fetch_ohlcv.call_count == 1
assert not log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
# test caching
res = await exchange._async_get_candle_history(pair, "5m")
assert exchange._api_async.fetch_ohlcv.call_count == 1
assert log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
# exchange = Exchange(default_conf)
await async_ccxt_exception(mocker, default_conf, MagicMock(),
"_async_get_candle_history", "fetch_ohlcv",
pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
api_mock = MagicMock()
with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
await exchange._async_get_candle_history(pair, "5m",
(arrow.utcnow().timestamp - 2000) * 1000)
@pytest.mark.asyncio
async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
""" Test empty exchange result """
tick = []
caplog.set_level(logging.DEBUG)
exchange = get_patched_exchange(mocker, default_conf)
# Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro([])
exchange = Exchange(default_conf)
pair = 'ETH/BTC'
res = await exchange._async_get_candle_history(pair, "5m")
assert type(res) is tuple
assert len(res) == 2
assert res[0] == pair
assert res[1] == tick
assert exchange._api_async.fetch_ohlcv.call_count == 1
@pytest.mark.asyncio
async def test_async_get_candles_history(default_conf, mocker):
tick = [
[
1511686200000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
async def mock_get_candle_hist(pair, tick_interval, since_ms=None):
return (pair, tick)
exchange = get_patched_exchange(mocker, default_conf)
# Monkey-patch async function
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
exchange._async_get_candle_history = Mock(wraps=mock_get_candle_hist)
pairs = ['ETH/BTC', 'XRP/BTC']
res = await exchange.async_get_candles_history(pairs, "5m")
assert type(res) is list
assert len(res) == 2
assert type(res[0]) is tuple
assert res[0][0] == pairs[0]
assert res[0][1] == tick
assert res[1][0] == pairs[1]
assert res[1][1] == tick
assert exchange._async_get_candle_history.call_count == 2
def test_get_order_book(default_conf, mocker, order_book_l2):
default_conf['exchange']['name'] = 'binance'
api_mock = MagicMock()
api_mock.fetch_l2_order_book = order_book_l2
exchange = get_patched_exchange(mocker, default_conf, api_mock)
order_book = exchange.get_order_book(pair='ETH/BTC', limit=10)
assert 'bids' in order_book
assert 'asks' in order_book
assert len(order_book['bids']) == 10
assert len(order_book['asks']) == 10
def test_get_order_book_exception(default_conf, mocker):
api_mock = MagicMock()
with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(TemporaryError):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order_book(pair='ETH/BTC', limit=50)
with pytest.raises(OperationalException):
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order_book(pair='ETH/BTC', limit=50)
def make_fetch_ohlcv_mock(data): def make_fetch_ohlcv_mock(data):
def fetch_ohlcv_mock(pair, timeframe, since): def fetch_ohlcv_mock(pair, timeframe, since):
if since: if since:
@@ -369,7 +780,7 @@ def make_fetch_ohlcv_mock(data):
return fetch_ohlcv_mock return fetch_ohlcv_mock
def test_get_ticker_history(default_conf, mocker): def test_get_candle_history(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
tick = [ tick = [
[ [
@@ -386,7 +797,7 @@ def test_get_ticker_history(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# retrieve original ticker # retrieve original ticker
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686200000 assert ticks[0][0] == 1511686200000
assert ticks[0][1] == 1 assert ticks[0][1] == 1
assert ticks[0][2] == 2 assert ticks[0][2] == 2
@@ -408,7 +819,7 @@ def test_get_ticker_history(default_conf, mocker):
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick)) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686210000 assert ticks[0][0] == 1511686210000
assert ticks[0][1] == 6 assert ticks[0][1] == 6
assert ticks[0][2] == 7 assert ticks[0][2] == 7
@@ -416,20 +827,17 @@ def test_get_ticker_history(default_conf, mocker):
assert ticks[0][4] == 9 assert ticks[0][4] == 9
assert ticks[0][5] == 10 assert ticks[0][5] == 10
with pytest.raises(TemporaryError): # test retrier ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError) "get_candle_history", "fetch_ohlcv",
pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported)
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# new symbol to get around cache exchange.get_candle_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
exchange.get_ticker_history('ABCD/BTC', default_conf['ticker_interval'])
with pytest.raises(OperationalException):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# new symbol to get around cache
exchange.get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
def test_get_ticker_history_sort(default_conf, mocker): def test_get_candle_history_sort(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
# GDAX use-case (real data from GDAX) # GDAX use-case (real data from GDAX)
@@ -452,7 +860,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort # Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527830400000 assert ticks[0][0] == 1527830400000
assert ticks[0][1] == 0.07649 assert ticks[0][1] == 0.07649
assert ticks[0][2] == 0.07651 assert ticks[0][2] == 0.07651
@@ -485,7 +893,7 @@ def test_get_ticker_history_sort(default_conf, mocker):
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort # Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527827700000 assert ticks[0][0] == 1527827700000
assert ticks[0][1] == 0.07659999 assert ticks[0][1] == 0.07659999
assert ticks[0][2] == 0.0766 assert ticks[0][2] == 0.0766
@@ -515,24 +923,15 @@ def test_cancel_order(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
with pytest.raises(TemporaryError):
api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.cancel_order(order_id='_', pair='TKN/BTC') exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1 assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException): ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError) "cancel_order", "cancel_order",
exchange = get_patched_exchange(mocker, default_conf, api_mock) order_id='_', pair='TKN/BTC')
exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == 1
def test_get_order(default_conf, mocker): def test_get_order(default_conf, mocker):
@@ -550,28 +949,19 @@ def test_get_order(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_order('X', 'TKN/BTC') == 456 assert exchange.get_order('X', 'TKN/BTC') == 456
with pytest.raises(TemporaryError):
api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC') exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException): ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError) 'get_order', 'fetch_order',
exchange = get_patched_exchange(mocker, default_conf, api_mock) order_id='_', pair='TKN/BTC')
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1
def test_name(default_conf, mocker): def test_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
exchange = Exchange(default_conf) exchange = Exchange(default_conf)
@@ -579,16 +969,14 @@ def test_name(default_conf, mocker):
def test_id(default_conf, mocker): def test_id(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
exchange = Exchange(default_conf) exchange = Exchange(default_conf)
assert exchange.id == 'binance' assert exchange.id == 'binance'
def test_get_pair_detail_url(default_conf, mocker, caplog): def test_get_pair_detail_url(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
exchange = Exchange(default_conf) exchange = Exchange(default_conf)
@@ -651,19 +1039,12 @@ def test_get_trades_for_order(default_conf, mocker):
assert len(orders) == 1 assert len(orders) == 1
assert orders[0]['price'] == 165 assert orders[0]['price'] == 165
# test Exceptions ccxt_exceptionhandlers(mocker, default_conf, api_mock,
with pytest.raises(OperationalException): 'get_trades_for_order', 'fetch_my_trades',
api_mock = MagicMock() order_id=order_id, pair='LTC/BTC', since=since)
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
with pytest.raises(TemporaryError): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
api_mock = MagicMock() assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == []
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
assert api_mock.fetch_my_trades.call_count == API_RETRY_COUNT + 1
def test_get_markets(default_conf, mocker, markets): def test_get_markets(default_conf, mocker, markets):
@@ -677,19 +1058,8 @@ def test_get_markets(default_conf, mocker, markets):
assert ret[0]["id"] == "ethbtc" assert ret[0]["id"] == "ethbtc"
assert ret[0]["symbol"] == "ETH/BTC" assert ret[0]["symbol"] == "ETH/BTC"
# test Exceptions ccxt_exceptionhandlers(mocker, default_conf, api_mock,
with pytest.raises(OperationalException): 'get_markets', 'fetch_markets')
api_mock = MagicMock()
api_mock.fetch_markets = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_markets()
with pytest.raises(TemporaryError):
api_mock = MagicMock()
api_mock.fetch_markets = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_markets()
assert api_mock.fetch_markets.call_count == API_RETRY_COUNT + 1
def test_get_fee(default_conf, mocker): def test_get_fee(default_conf, mocker):
@@ -704,28 +1074,5 @@ def test_get_fee(default_conf, mocker):
assert exchange.get_fee() == 0.025 assert exchange.get_fee() == 0.025
# test Exceptions ccxt_exceptionhandlers(mocker, default_conf, api_mock,
with pytest.raises(OperationalException): 'get_fee', 'calculate_fee')
api_mock = MagicMock()
api_mock.calculate_fee = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_fee()
with pytest.raises(TemporaryError):
api_mock = MagicMock()
api_mock.calculate_fee = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_fee()
assert api_mock.calculate_fee.call_count == API_RETRY_COUNT + 1
def test_get_amount_lots(default_conf, mocker):
api_mock = MagicMock()
api_mock.amount_to_lots = MagicMock(return_value=1.0)
api_mock.markets = None
marketmock = MagicMock()
api_mock.load_markets = marketmock
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_amount_lots('LTC/BTC', 1.54) == 1
assert marketmock.call_count == 1

View File

@@ -0,0 +1,21 @@
# pragma pylint: disable=missing-docstring, C0103
from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
def test_dataframe_correct_length(result):
dataframe = parse_ticker_dataframe(result)
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
def test_dataframe_correct_columns(result):
assert result.columns.tolist() == \
['date', 'open', 'high', 'low', 'close', 'volume']
def test_parse_ticker_dataframe(ticker_history):
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
# Test file with BV data
dataframe = parse_ticker_dataframe(ticker_history)
assert dataframe.columns.tolist() == columns

View File

@@ -3,20 +3,21 @@
import json import json
import math import math
import random import random
import pytest
from copy import deepcopy
from typing import List from typing import List
from unittest.mock import MagicMock from unittest.mock import MagicMock
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytest
from arrow import Arrow from arrow import Arrow
from freqtrade import optimize, constants, DependencyException from freqtrade import DependencyException, constants, optimize
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments, TimeRange from freqtrade.arguments import Arguments, TimeRange
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
start)
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import log_has, patch_exchange
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.default_strategy import DefaultStrategy
def get_args(args) -> List[str]: def get_args(args) -> List[str]:
@@ -95,7 +96,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
'stake_amount': config['stake_amount'], 'stake_amount': config['stake_amount'],
'processed': processed, 'processed': processed,
'max_open_trades': 1, 'max_open_trades': 1,
'realistic': True 'position_stacking': False
} }
) )
# results :: <class 'pandas.core.frame.DataFrame'> # results :: <class 'pandas.core.frame.DataFrame'>
@@ -109,7 +110,7 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
return pairdata return pairdata
# use for mock freqtrade.exchange.get_ticker_history' # use for mock ccxt.fetch_ohlvc'
def _load_pair_as_ticks(pair, tickfreq): def _load_pair_as_ticks(pair, tickfreq):
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair]) ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
ticks = trim_dictlist(ticks, -201) ticks = trim_dictlist(ticks, -201)
@@ -126,7 +127,7 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
'stake_amount': conf['stake_amount'], 'stake_amount': conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data), 'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 10, 'max_open_trades': 10,
'realistic': True, 'position_stacking': False,
'record': record 'record': record
} }
@@ -144,7 +145,7 @@ def _trend(signals, buy_value, sell_value):
return signals return signals
def _trend_alternate(dataframe=None): def _trend_alternate(dataframe=None, metadata=None):
signals = dataframe signals = dataframe
low = signals['low'] low = signals['low']
n = len(low) n = len(low)
@@ -162,9 +163,6 @@ def _trend_alternate(dataframe=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:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@@ -192,8 +190,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'live' not in config assert 'live' not in config
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' not in config assert 'position_stacking' not in config
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples) assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
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.record_tuples)
@@ -203,9 +201,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@@ -217,7 +212,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'backtesting', 'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--live', '--live',
'--realistic-simulation', '--enable-position-stacking',
'--disable-max-market-positions',
'--refresh-pairs-cached', '--refresh-pairs-cached',
'--timerange', ':100', '--timerange', ':100',
'--export', '/bar/foo', '--export', '/bar/foo',
@@ -245,9 +241,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'live' in config assert 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples) assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' in config assert 'position_stacking' in config
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples) assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
assert 'use_max_market_positions' in config
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
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.record_tuples)
@@ -270,15 +269,10 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
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
Test setup_configuration() function
"""
conf = deepcopy(default_conf)
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf) read_data=json.dumps(default_conf)
)) ))
args = [ args = [
@@ -292,9 +286,6 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog
def test_start(mocker, fee, default_conf, caplog) -> None: def test_start(mocker, fee, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
@@ -317,26 +308,19 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
def test_backtesting_init(mocker, default_conf) -> None: def test_backtesting_init(mocker, default_conf) -> None:
"""
Test Backtesting._init() method
"""
patch_exchange(mocker) patch_exchange(mocker)
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf assert backtesting.config == default_conf
assert isinstance(backtesting.analyze, Analyze)
assert backtesting.ticker_interval == '5m' assert backtesting.ticker_interval == '5m'
assert callable(backtesting.tickerdata_to_dataframe) assert callable(backtesting.tickerdata_to_dataframe)
assert callable(backtesting.populate_buy_trend) assert callable(backtesting.advise_buy)
assert callable(backtesting.populate_sell_trend) assert callable(backtesting.advise_sell)
get_fee.assert_called() get_fee.assert_called()
assert backtesting.fee == 0.5 assert backtesting.fee == 0.5
def test_tickerdata_to_dataframe(default_conf, mocker) -> None: def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
"""
Test Backtesting.tickerdata_to_dataframe() method
"""
patch_exchange(mocker) patch_exchange(mocker)
timerange = TimeRange(None, 'line', 0, -100) timerange = TimeRange(None, 'line', 0, -100)
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
@@ -346,16 +330,13 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
data = backtesting.tickerdata_to_dataframe(tickerlist) data = backtesting.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 99 assert len(data['UNITTEST/BTC']) == 99
# Load Analyze to compare the result between Backtesting function and Analyze are the same # Load strategy to compare the result between Backtesting function and strategy are the same
analyze = Analyze(default_conf) strategy = DefaultStrategy(default_conf)
data2 = analyze.tickerdata_to_dataframe(tickerlist) data2 = strategy.tickerdata_to_dataframe(tickerlist)
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC']) assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
def test_get_timeframe(default_conf, mocker) -> None: def test_get_timeframe(default_conf, mocker) -> None:
"""
Test Backtesting.get_timeframe() method
"""
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@@ -372,9 +353,6 @@ def test_get_timeframe(default_conf, mocker) -> None:
def test_generate_text_table(default_conf, mocker): def test_generate_text_table(default_conf, mocker):
"""
Test Backtesting.generate_text_table() method
"""
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@@ -390,29 +368,94 @@ def test_generate_text_table(default_conf, mocker):
) )
result_str = ( result_str = (
'| pair | buy count | avg profit % | ' '| pair | buy count | avg profit % | cum profit % | '
'total profit BTC | avg duration | profit | loss |\n' 'total profit BTC | avg duration | profit | loss |\n'
'|:--------|------------:|---------------:|' '|:--------|------------:|---------------:|---------------:|'
'-------------------:|---------------:|---------:|-------:|\n' '-------------------:|:---------------|---------:|-------:|\n'
'| ETH/BTC | 2 | 15.00 | ' '| ETH/BTC | 2 | 15.00 | 30.00 | '
'0.60000000 | 20.0 | 2 | 0 |\n' '0.60000000 | 0:20:00 | 2 | 0 |\n'
'| TOTAL | 2 | 15.00 | ' '| TOTAL | 2 | 15.00 | 30.00 | '
'0.60000000 | 20.0 | 2 | 0 |' '0.60000000 | 0:20:00 | 2 | 0 |'
) )
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
def test_backtesting_start(default_conf, mocker, caplog) -> None: def test_generate_text_table_sell_reason(default_conf, mocker):
""" patch_exchange(mocker)
Test Backtesting.start() method backtesting = Backtesting(default_conf)
"""
results = 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]
}
)
result_str = (
'| Sell Reason | Count |\n'
'|:--------------|--------:|\n'
'| roi | 2 |\n'
'| stop_loss | 1 |'
)
assert backtesting._generate_text_table_sell_reason(
data={'ETH/BTC': {}}, results=results) == result_str
def test_generate_text_table_strategyn(default_conf, mocker):
"""
Test Backtesting.generate_text_table_sell_reason() method
"""
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
results = {}
results['ETH/BTC'] = 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]
}
)
results['LTC/BTC'] = pd.DataFrame(
{
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
'profit_percent': [0.4, 0.2, 0.3],
'profit_abs': [0.4, 0.4, 0.5],
'trade_duration': [15, 30, 15],
'profit': [4, 1, 0],
'loss': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
}
)
result_str = (
'| Strategy | buy count | avg profit % | cum profit % '
'| total profit BTC | avg duration | profit | loss |\n'
'|:-----------|------------:|---------------:|---------------:'
'|-------------------:|:---------------|---------:|-------:|\n'
'| ETH/BTC | 3 | 20.00 | 60.00 '
'| 1.10000000 | 0:17:00 | 3 | 0 |\n'
'| LTC/BTC | 3 | 30.00 | 90.00 '
'| 1.30000000 | 0:20:00 | 3 | 0 |'
)
print(backtesting._generate_text_table_strategy(all_results=results))
assert backtesting._generate_text_table_strategy(all_results=results) == result_str
def test_backtesting_start(default_conf, mocker, caplog) -> None:
def get_timeframe(input1, input2): def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', mocked_load_data) mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting', 'freqtrade.optimize.backtesting.Backtesting',
@@ -421,15 +464,14 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
get_timeframe=get_timeframe, get_timeframe=get_timeframe,
) )
conf = deepcopy(default_conf) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = 1
conf['ticker_interval'] = 1 default_conf['live'] = False
conf['live'] = False default_conf['datadir'] = None
conf['datadir'] = None default_conf['export'] = None
conf['export'] = None default_conf['timerange'] = '-100'
conf['timerange'] = '-100'
backtesting = Backtesting(conf) backtesting = Backtesting(default_conf)
backtesting.start() backtesting.start()
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
exists = [ exists = [
@@ -444,16 +486,11 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method if no data is found
"""
def get_timeframe(input1, input2): def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={})) mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting', 'freqtrade.optimize.backtesting.Backtesting',
@@ -462,15 +499,14 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
get_timeframe=get_timeframe, get_timeframe=get_timeframe,
) )
conf = deepcopy(default_conf) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = "1m"
conf['ticker_interval'] = "1m" default_conf['live'] = False
conf['live'] = False default_conf['datadir'] = None
conf['datadir'] = None default_conf['export'] = None
conf['export'] = None default_conf['timerange'] = '20180101-20180102'
conf['timerange'] = '20180101-20180102'
backtesting = Backtesting(conf) backtesting = Backtesting(default_conf)
backtesting.start() backtesting.start()
# check the logs, that will contain the backtest result # check the logs, that will contain the backtest result
@@ -478,31 +514,53 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
def test_backtest(default_conf, fee, mocker) -> None: def test_backtest(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method
"""
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
pair = 'UNITTEST/BTC'
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
data_processed = backtesting.tickerdata_to_dataframe(data)
results = backtesting.backtest( results = backtesting.backtest(
{ {
'stake_amount': default_conf['stake_amount'], 'stake_amount': default_conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data), 'processed': data_processed,
'max_open_trades': 10, 'max_open_trades': 10,
'realistic': True 'position_stacking': False
} }
) )
assert not results.empty assert not results.empty
assert len(results) == 2 assert len(results) == 2
expected = pd.DataFrame(
{'pair': [pair, pair],
'profit_percent': [0.00029975, 0.00056708],
'profit_abs': [1.49e-06, 7.6e-07],
'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime,
Arrow(2018, 1, 30, 3, 30, 0).datetime],
'close_time': [Arrow(2018, 1, 29, 22, 40, 0).datetime,
Arrow(2018, 1, 30, 4, 20, 0).datetime],
'open_index': [77, 183],
'close_index': [125, 193],
'trade_duration': [240, 50],
'open_at_end': [False, False],
'open_rate': [0.104445, 0.10302485],
'close_rate': [0.105, 0.10359999],
'sell_reason': [SellType.ROI, SellType.ROI]
})
pd.testing.assert_frame_equal(results, expected)
data_pair = data_processed[pair]
for _, t in results.iterrows():
ln = data_pair.loc[data_pair["date"] == t["open_time"]]
# Check open trade rate alignes to open rate
assert ln is not None
assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6)
# check close trade rate alignes to close rate
ln = data_pair.loc[data_pair["date"] == t["close_time"]]
assert round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6)
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method with 1 min ticker
"""
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@@ -515,7 +573,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
'stake_amount': default_conf['stake_amount'], 'stake_amount': default_conf['stake_amount'],
'processed': backtesting.tickerdata_to_dataframe(data), 'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 1, 'max_open_trades': 1,
'realistic': True 'position_stacking': False
} }
) )
assert not results.empty assert not results.empty
@@ -523,9 +581,6 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
def test_processed(default_conf, mocker) -> None: def test_processed(default_conf, mocker) -> None:
"""
Test Backtesting.backtest() method with offline data
"""
patch_exchange(mocker) patch_exchange(mocker)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
@@ -551,42 +606,42 @@ def test_backtest_ticks(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker) patch_exchange(mocker)
ticks = [1, 5] ticks = [1, 5]
fun = Backtesting(default_conf).populate_buy_trend fun = Backtesting(default_conf).advise_buy
for _ in ticks: for _ in ticks:
backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override backtesting.advise_buy = fun # Override
backtesting.populate_sell_trend = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert not results.empty assert not results.empty
def test_backtest_clash_buy_sell(mocker, default_conf): def test_backtest_clash_buy_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None, pair=None):
buy_value = 1 buy_value = 1
sell_value = 1 sell_value = 1
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override backtesting.advise_buy = fun # Override
backtesting.populate_sell_trend = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert results.empty assert results.empty
def test_backtest_only_sell(mocker, default_conf): def test_backtest_only_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy # Override the default buy trend function in our default_strategy
def fun(dataframe=None): def fun(dataframe=None, pair=None):
buy_value = 0 buy_value = 0
sell_value = 1 sell_value = 1
return _trend(dataframe, buy_value, sell_value) return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override backtesting.advise_buy = fun # Override
backtesting.populate_sell_trend = fun # Override backtesting.advise_sell = fun # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert results.empty assert results.empty
@@ -595,8 +650,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC') backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
backtesting = Backtesting(default_conf) backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override backtesting.advise_buy = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override backtesting.advise_sell = _trend_alternate # Override
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
backtesting._store_backtest_result("test_.json", results) backtesting._store_backtest_result("test_.json", results)
assert len(results) == 4 assert len(results) == 4
@@ -627,9 +682,15 @@ def test_backtest_record(default_conf, fee, mocker):
Arrow(2017, 11, 14, 22, 10, 00).datetime, Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime, Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime], Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"open_index": [1, 119, 153, 185], "open_index": [1, 119, 153, 185],
"close_index": [118, 151, 184, 199], "close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14]}) "trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True],
"sell_reason": [SellType.ROI, SellType.STOP_LOSS,
SellType.ROI, SellType.FORCE_SELL]
})
backtesting._store_backtest_result("backtest-result.json", results) backtesting._store_backtest_result("backtest-result.json", results)
assert len(results) == 4 assert len(results) == 4
# Assert file_dump_json was only called once # Assert file_dump_json was only called once
@@ -637,15 +698,32 @@ def test_backtest_record(default_conf, fee, mocker):
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
# reset test to test with strategy name
names = []
records = []
backtesting._store_backtest_result("backtest-result.json", results, "DefStrat")
assert len(results) == 4
# Assert file_dump_json was only called once
assert names == ['backtest-result-DefStrat.json']
records = records[0]
# Ensure records are of correct type
assert len(records) == 4
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records # Below follows just a typecheck of the schema/type of trade-records
oix = None oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur) in records: for (pair, profit, date_buy, date_sell, buy_index, dur,
openr, closer, open_at_end, sell_reason) in records:
assert pair == 'UNITTEST/BTC' assert pair == 'UNITTEST/BTC'
isinstance(profit, float) assert isinstance(profit, float)
# FIX: buy/sell should be converted to ints # FIX: buy/sell should be converted to ints
isinstance(date_buy, str) assert isinstance(date_buy, float)
isinstance(date_sell, str) assert isinstance(date_sell, float)
assert isinstance(openr, float)
assert isinstance(closer, float)
assert isinstance(open_at_end, bool)
assert isinstance(sell_reason, str)
isinstance(buy_index, pd._libs.tslib.Timestamp) isinstance(buy_index, pd._libs.tslib.Timestamp)
if oix: if oix:
assert buy_index > oix assert buy_index > oix
@@ -654,26 +732,21 @@ def test_backtest_record(default_conf, fee, mocker):
def test_backtest_start_live(default_conf, mocker, caplog): def test_backtest_start_live(default_conf, mocker, caplog):
conf = deepcopy(default_conf) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', async def load_pairs(pair, timeframe, since):
new=lambda s, n, i: _load_pair_as_ticks(n, i)) return _load_pair_as_ticks(pair, timeframe)
patch_exchange(mocker)
api_mock = MagicMock()
api_mock.fetch_ohlcv = load_pairs
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( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf) read_data=json.dumps(default_conf)
)) ))
args = MagicMock()
args.ticker_interval = 1
args.level = 10
args.live = True
args.datadir = None
args.export = None
args.strategy = 'DefaultStrategy'
args.timerange = '-100' # needed due to MagicMock malleability
args = [ args = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
@@ -682,7 +755,8 @@ def test_backtest_start_live(default_conf, mocker, caplog):
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--live', '--live',
'--timerange', '-100', '--timerange', '-100',
'--realistic-simulation' '--enable-position-stacking',
'--disable-max-market-positions'
] ]
args = get_args(args) args = get_args(args)
start(args) start(args)
@@ -691,14 +765,75 @@ def test_backtest_start_live(default_conf, mocker, caplog):
'Parameter -i/--ticker-interval detected ...', 'Parameter -i/--ticker-interval detected ...',
'Using ticker_interval: 1m ...', 'Using ticker_interval: 1m ...',
'Parameter -l/--live detected ...', 'Parameter -l/--live detected ...',
'Using max_open_trades: 1 ...', '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 folder: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...', 'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...', 'Using stake_amount: 0.001 ...',
'Downloading data for all pairs in whitelist ...', 'Downloading data for all pairs in whitelist ...',
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..', 'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Parameter --realistic-simulation detected ...' 'Parameter --enable-position-stacking detected ...'
]
for line in exists:
assert log_has(line, caplog.record_tuples)
def test_backtest_start_multi_strat(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
async def load_pairs(pair, timeframe, since):
return _load_pair_as_ticks(pair, timeframe)
api_mock = MagicMock()
api_mock.fetch_ohlcv = load_pairs
patch_exchange(mocker, api_mock)
backtestmock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
gen_table_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock)
gen_strattable_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy',
gen_strattable_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
args = [
'--config', 'config.json',
'--datadir', 'freqtrade/tests/testdata',
'backtesting',
'--ticker-interval', '1m',
'--live',
'--timerange', '-100',
'--enable-position-stacking',
'--disable-max-market-positions',
'--strategy-list',
'DefaultStrategy',
'TestStrategy',
]
args = get_args(args)
start(args)
# 2 backtests, 4 tables
assert backtestmock.call_count == 2
assert gen_table_mock.call_count == 4
assert gen_strattable_mock.call_count == 1
# check the logs, that will contain the backtest result
exists = [
'Parameter -i/--ticker-interval detected ...',
'Using ticker_interval: 1m ...',
'Parameter -l/--live detected ...',
'Ignoring max_open_trades (--disable-max-market-positions was used) ...',
'Parameter --timerange detected: -100 ...',
'Using data folder: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Downloading data for all pairs in whitelist ...',
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Parameter --enable-position-stacking detected ...',
'Running backtesting for Strategy DefaultStrategy',
'Running backtesting for Strategy TestStrategy',
] ]
for line in exists: for line in exists:

View File

@@ -1,7 +1,5 @@
# pragma pylint: disable=missing-docstring,W0212,C0103 # pragma pylint: disable=missing-docstring,W0212,C0103
import os import os
import signal
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pandas as pd import pandas as pd
@@ -13,52 +11,32 @@ from freqtrade.strategy.resolver import StrategyResolver
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import log_has, patch_exchange
from freqtrade.tests.optimize.test_backtesting import get_args from freqtrade.tests.optimize.test_backtesting import get_args
# Avoid to reinit the same object again and again
_HYPEROPT_INITIALIZED = False
_HYPEROPT = None
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
def init_hyperopt(default_conf, mocker): def hyperopt(default_conf, mocker):
global _HYPEROPT_INITIALIZED, _HYPEROPT patch_exchange(mocker)
if not _HYPEROPT_INITIALIZED: return Hyperopt(default_conf)
patch_exchange(mocker)
_HYPEROPT = Hyperopt(default_conf)
_HYPEROPT_INITIALIZED = True
# Functions for recurrent object patching # Functions for recurrent object patching
def create_trials(mocker) -> None: def create_trials(mocker, hyperopt) -> None:
""" """
When creating trials, mock the hyperopt Trials so that *by default* When creating trials, mock the hyperopt Trials so that *by default*
- we don't create any pickle'd files in the filesystem - we don't create any pickle'd files in the filesystem
- 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 = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False) mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1) mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True) mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None) mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
return mocker.Mock( return [{'loss': 1, 'result': 'foo', 'params': {}}]
results=[
{
'loss': 1,
'result': 'foo',
'status': 'ok'
}
],
best_trial={'misc': {'vals': {'adx': 999}}}
)
# Unit tests
def test_start(mocker, default_conf, caplog) -> None: def test_start(mocker, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock() start_mock = MagicMock()
mocker.patch( mocker.patch(
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
@@ -87,11 +65,32 @@ def test_start(mocker, default_conf, caplog) -> None:
assert start_mock.call_count == 1 assert start_mock.call_count == 1
def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None: def test_start_failure(mocker, default_conf, caplog) -> None:
""" start_mock = MagicMock()
Test Hyperopt.calculate_loss() mocker.patch(
""" 'freqtrade.configuration.Configuration._load_config_file',
hyperopt = _HYPEROPT lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
patch_exchange(mocker)
args = [
'--config', 'config.json',
'--strategy', 'TestStrategy',
'hyperopt',
'--epochs', '5'
]
args = get_args(args)
StrategyResolver({'strategy': 'DefaultStrategy'})
with pytest.raises(ValueError):
start(args)
assert log_has(
"Please don't use --strategy for hyperopt.",
caplog.record_tuples
)
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
StrategyResolver({'strategy': 'DefaultStrategy'}) StrategyResolver({'strategy': 'DefaultStrategy'})
correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20)
@@ -101,20 +100,13 @@ def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
assert under > correct assert under > correct
def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None: def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None:
"""
Test Hyperopt.calculate_loss()
"""
hyperopt = _HYPEROPT
shorter = hyperopt.calculate_loss(1, 100, 20) shorter = hyperopt.calculate_loss(1, 100, 20)
longer = hyperopt.calculate_loss(1, 100, 30) longer = hyperopt.calculate_loss(1, 100, 30)
assert shorter < longer assert shorter < longer
def test_loss_calculation_has_limited_profit(init_hyperopt) -> None: def test_loss_calculation_has_limited_profit(hyperopt) -> None:
hyperopt = _HYPEROPT
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20)
under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20)
@@ -122,8 +114,7 @@ def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
assert under > correct assert under > correct
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None: def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2 hyperopt.current_best_loss = 2
hyperopt.log_results( hyperopt.log_results(
{ {
@@ -134,11 +125,10 @@ def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
} }
) )
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert ' 1/2: foo. Loss 1.00000'in out assert ' 1/2: foo. Loss 1.00000' in out
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None: def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2 hyperopt.current_best_loss = 2
hyperopt.log_results( hyperopt.log_results(
{ {
@@ -148,166 +138,23 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
assert caplog.record_tuples == [] assert caplog.record_tuples == []
def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None: def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None:
fmin_result = { trials = create_trials(mocker, hyperopt)
"macd_below_zero": 0, mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
"adx": 1,
"adx-value": 15.0,
"fastd": 1,
"fastd-value": 40.0,
"green_candle": 1,
"mfi": 0,
"over_sar": 0,
"rsi": 1,
"rsi-value": 37.0,
"trigger": 2,
"uptrend_long_ema": 1,
"uptrend_short_ema": 0,
"uptrend_sma": 0,
"stoploss": -0.1,
"roi_t1": 1,
"roi_t2": 2,
"roi_t3": 3,
"roi_p1": 1,
"roi_p2": 2,
"roi_p3": 3,
}
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
patch_exchange(mocker)
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
exists = [
'Best parameters:',
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
'"fastd": {\n "enabled": true,\n "value": 40.0\n },',
'"green_candle": {\n "enabled": true\n },',
'"macd_below_zero": {\n "enabled": false\n },',
'"mfi": {\n "enabled": false\n },',
'"over_sar": {\n "enabled": false\n },',
'"roi_p1": 1.0,',
'"roi_p2": 2.0,',
'"roi_p3": 3.0,',
'"roi_t1": 1.0,',
'"roi_t2": 2.0,',
'"roi_t3": 3.0,',
'"rsi": {\n "enabled": true,\n "value": 37.0\n },',
'"stoploss": -0.1,',
'"trigger": {\n "type": "faststoch10"\n },',
'"uptrend_long_ema": {\n "enabled": true\n },',
'"uptrend_short_ema": {\n "enabled": false\n },',
'"uptrend_sma": {\n "enabled": false\n }',
'ROI table:\n{0: 6.0, 3.0: 3.0, 5.0: 1.0, 6.0: 0}',
'Best Result:\nfoo'
]
for line in exists:
assert line in caplog.text
def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
patch_exchange(mocker)
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
exists = [
'Best Result:',
'Sorry, Hyperopt was not able to find good parameters. Please try with more epochs '
'(param: -e).',
]
for line in exists:
assert line in caplog.text
def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, default_conf) -> None:
trials = create_trials(mocker)
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=True)
mocker.patch('freqtrade.optimize.hyperopt.len', return_value=len(trials.results))
mock_read = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.read_trials',
return_value=trials
)
mock_save = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.save_trials',
return_value=None
)
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
patch_exchange(mocker)
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = trials hyperopt.trials = trials
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_read.assert_called_once()
mock_save.assert_called_once()
current_tries = hyperopt.current_tries
total_tries = hyperopt.total_tries
assert current_tries == len(trials.results)
assert total_tries == (current_tries + len(trials.results))
def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
create_trials(mocker)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
hyperopt = _HYPEROPT
mocker.patch('freqtrade.optimize.hyperopt.open', return_value=hyperopt.trials_file)
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 Trials to \'{}\''.format(trials_file), 'Saving 1 evaluations to \'{}\''.format(trials_file),
caplog.record_tuples caplog.record_tuples
) )
mock_dump.assert_called_once() mock_dump.assert_called_once()
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None: def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None:
trials = create_trials(mocker) trials = create_trials(mocker, hyperopt)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials) mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
hyperopt = _HYPEROPT
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(
@@ -315,11 +162,10 @@ def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
caplog.record_tuples caplog.record_tuples
) )
assert hyperopt_trial == trials assert hyperopt_trial == trials
mock_open.assert_called_once()
mock_load.assert_called_once() mock_load.assert_called_once()
def test_roi_table_generation(init_hyperopt) -> None: def test_roi_table_generation(hyperopt) -> None:
params = { params = {
'roi_t1': 5, 'roi_t1': 5,
'roi_t2': 10, 'roi_t2': 10,
@@ -329,36 +175,35 @@ def test_roi_table_generation(init_hyperopt) -> None:
'roi_p3': 3, 'roi_p3': 3,
} }
hyperopt = _HYPEROPT
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None: def test_start_calls_optimizer(mocker, default_conf, caplog) -> None:
trials = create_trials(mocker) dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1))
parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}])
)
patch_exchange(mocker) patch_exchange(mocker)
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
conf = deepcopy(default_conf) default_conf.update({'config': 'config.json.example'})
conf.update({'config': 'config.json.example'}) default_conf.update({'epochs': 1})
conf.update({'epochs': 1}) default_conf.update({'timerange': None})
conf.update({'timerange': None}) default_conf.update({'spaces': 'all'})
conf.update({'spaces': 'all'})
hyperopt = Hyperopt(conf) hyperopt = Hyperopt(default_conf)
hyperopt.trials = trials
hyperopt.tickerdata_to_dataframe = MagicMock() hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start() hyperopt.start()
mock_fmin.assert_called_once() parallel.assert_called_once()
assert 'Best result:\nfoo result\nwith values:\n{}' in caplog.text
assert dumper.called
def test_format_results(init_hyperopt): def test_format_results(hyperopt):
"""
Test Hyperopt.format_results()
"""
# Test with BTC as stake_currency # Test with BTC as stake_currency
trades = [ trades = [
('ETH/BTC', 2, 2, 123), ('ETH/BTC', 2, 2, 123),
@@ -368,7 +213,7 @@ def test_format_results(init_hyperopt):
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration'] labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
df = pd.DataFrame.from_records(trades, columns=labels) df = pd.DataFrame.from_records(trades, columns=labels)
result = _HYPEROPT.format_results(df) result = hyperopt.format_results(df)
assert result.find(' 66.67%') assert result.find(' 66.67%')
assert result.find('Total profit 1.00000000 BTC') assert result.find('Total profit 1.00000000 BTC')
assert result.find('2.0000Σ %') assert result.find('2.0000Σ %')
@@ -380,117 +225,61 @@ def test_format_results(init_hyperopt):
('XPR/EUR', -1, -2, -246) ('XPR/EUR', -1, -2, -246)
] ]
df = pd.DataFrame.from_records(trades, columns=labels) df = pd.DataFrame.from_records(trades, columns=labels)
result = _HYPEROPT.format_results(df) result = hyperopt.format_results(df)
assert result.find('Total profit 1.00000000 EUR') assert result.find('Total profit 1.00000000 EUR')
def test_signal_handler(mocker, init_hyperopt): def test_has_space(hyperopt):
""" hyperopt.config.update({'spaces': ['buy', 'roi']})
Test Hyperopt.signal_handler() assert hyperopt.has_space('roi')
""" assert hyperopt.has_space('buy')
m = MagicMock() assert not hyperopt.has_space('stoploss')
mocker.patch('sys.exit', m)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.save_trials', m)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
hyperopt = _HYPEROPT hyperopt.config.update({'spaces': ['all']})
hyperopt.signal_handler(signal.SIGTERM, None) assert hyperopt.has_space('buy')
assert m.call_count == 3
def test_has_space(init_hyperopt): def test_populate_indicators(hyperopt) -> None:
"""
Test Hyperopt.has_space() method
"""
_HYPEROPT.config.update({'spaces': ['buy', 'roi']})
assert _HYPEROPT.has_space('roi')
assert _HYPEROPT.has_space('buy')
assert not _HYPEROPT.has_space('stoploss')
_HYPEROPT.config.update({'spaces': ['all']})
assert _HYPEROPT.has_space('buy')
def test_populate_indicators(init_hyperopt) -> None:
"""
Test Hyperopt.populate_indicators()
"""
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick} tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
# Check if some indicators are generated. We will not test all of them # Check if some indicators are generated. We will not test all of them
assert 'adx' in dataframe assert 'adx' in dataframe
assert 'ao' in dataframe assert 'mfi' in dataframe
assert 'cci' in dataframe assert 'rsi' in dataframe
def test_buy_strategy_generator(init_hyperopt) -> None: def test_buy_strategy_generator(hyperopt) -> None:
"""
Test Hyperopt.buy_strategy_generator()
"""
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick} tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) dataframes = hyperopt.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'})
populate_buy_trend = _HYPEROPT.buy_strategy_generator( populate_buy_trend = hyperopt.buy_strategy_generator(
{ {
'uptrend_long_ema': { 'adx-value': 20,
'enabled': True 'fastd-value': 20,
}, 'mfi-value': 20,
'macd_below_zero': { 'rsi-value': 20,
'enabled': True 'adx-enabled': True,
}, 'fastd-enabled': True,
'uptrend_short_ema': { 'mfi-enabled': True,
'enabled': True 'rsi-enabled': True,
}, 'trigger': 'bb_lower'
'mfi': {
'enabled': True,
'value': 20
},
'fastd': {
'enabled': True,
'value': 20
},
'adx': {
'enabled': True,
'value': 20
},
'rsi': {
'enabled': True,
'value': 20
},
'over_sar': {
'enabled': True,
},
'green_candle': {
'enabled': True,
},
'uptrend_sma': {
'enabled': True,
},
'trigger': {
'type': 'lower_bb'
}
} }
) )
result = populate_buy_trend(dataframe) result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'})
# Check if some indicators are generated. We will not test all of them # Check if some indicators are generated. We will not test all of them
assert 'buy' in result assert 'buy' in result
assert 1 in result['buy'] assert 1 in result['buy']
def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: def test_generate_optimizer(mocker, default_conf) -> None:
""" default_conf.update({'config': 'config.json.example'})
Test Hyperopt.generate_optimizer() function default_conf.update({'timerange': None})
""" default_conf.update({'spaces': 'all'})
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
trades = [ trades = [
('POWR/BTC', 0.023117, 0.000233, 100) ('POWR/BTC', 0.023117, 0.000233, 100)
@@ -503,35 +292,33 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
MagicMock(return_value=backtest_result) MagicMock(return_value=backtest_result)
) )
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
optimizer_param = { optimizer_param = {
'adx': {'enabled': False}, 'adx-value': 0,
'fastd': {'enabled': True, 'value': 35.0}, 'fastd-value': 35,
'green_candle': {'enabled': True}, 'mfi-value': 0,
'macd_below_zero': {'enabled': True}, 'rsi-value': 0,
'mfi': {'enabled': False}, 'adx-enabled': False,
'over_sar': {'enabled': False}, 'fastd-enabled': True,
'roi_p1': 0.01, 'mfi-enabled': False,
'roi_p2': 0.01, 'rsi-enabled': False,
'roi_p3': 0.1, 'trigger': 'macd_cross_signal',
'roi_t1': 60.0, 'roi_t1': 60.0,
'roi_t2': 30.0, 'roi_t2': 30.0,
'roi_t3': 20.0, 'roi_t3': 20.0,
'rsi': {'enabled': False}, 'roi_p1': 0.01,
'roi_p2': 0.01,
'roi_p3': 0.1,
'stoploss': -0.4, 'stoploss': -0.4,
'trigger': {'type': 'macd_cross_signal'},
'uptrend_long_ema': {'enabled': False},
'uptrend_short_ema': {'enabled': True},
'uptrend_sma': {'enabled': True}
} }
response_expected = { response_expected = {
'loss': 1.9840569076926293, 'loss': 1.9840569076926293,
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' 'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
'(0.0231Σ%). Avg duration 100.0 mins.', '(0.0231Σ%). Avg duration 100.0 mins.',
'status': 'ok' 'params': optimizer_param
} }
hyperopt = Hyperopt(conf) hyperopt = Hyperopt(default_conf)
generate_optimizer_value = hyperopt.generate_optimizer(optimizer_param) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
assert generate_optimizer_value == response_expected assert generate_optimizer_value == response_expected

View File

@@ -3,16 +3,19 @@
import json import json
import os import os
import uuid import uuid
import arrow
from shutil import copyfile from shutil import copyfile
import arrow
from freqtrade import optimize from freqtrade import optimize
from freqtrade.misc import file_dump_json
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
load_cached_data_for_updating
from freqtrade.arguments import TimeRange from freqtrade.arguments import TimeRange
from freqtrade.tests.conftest import log_has, get_patched_exchange from freqtrade.misc import file_dump_json
from freqtrade.optimize.__init__ import (download_backtesting_testdata,
download_pairs,
load_cached_data_for_updating,
load_tickerdata_file,
make_testdata_path, trim_tickerlist)
from freqtrade.tests.conftest import get_patched_exchange, log_has
# Change this if modifying UNITTEST/BTC testdatafile # Change this if modifying UNITTEST/BTC testdatafile
_BTC_UNITTEST_LENGTH = 13681 _BTC_UNITTEST_LENGTH = 13681
@@ -50,10 +53,7 @@ def _clean_test_file(file: str) -> None:
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None: def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
""" mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
Test load_data() with 30 min ticker
"""
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json') file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m') optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
@@ -63,10 +63,7 @@ def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) ->
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None: def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
""" mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
Test load_data() with 5 min ticker
"""
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json') file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
_backup_file(file, copy_file=True) _backup_file(file, copy_file=True)
@@ -77,11 +74,7 @@ def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) ->
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)
Test load_data() with 1 min ticker
"""
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', 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)
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
@@ -94,7 +87,7 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_co
""" """
Test load_data() with 1 min ticker Test load_data() with 1 min ticker
""" """
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
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')
@@ -125,7 +118,7 @@ def test_testdata_path() -> None:
def test_download_pairs(ticker_history, mocker, default_conf) -> None: def test_download_pairs(ticker_history, mocker, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
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')
@@ -268,7 +261,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None: def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata', mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
side_effect=BaseException('File Error')) side_effect=BaseException('File Error'))
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
@@ -286,7 +279,7 @@ def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf)
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None: def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
# Download a 1 min ticker file # Download a 1 min ticker file
@@ -311,7 +304,7 @@ def test_download_backtesting_testdata2(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_ticker_history', return_value=tick) mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m') download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m')
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m') download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')
@@ -418,10 +411,6 @@ def test_trim_tickerlist() -> None:
def test_file_dump_json() -> None: def test_file_dump_json() -> None:
"""
Test file_dump_json()
:return: None
"""
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
'test_{id}.json'.format(id=str(uuid.uuid4()))) 'test_{id}.json'.format(id=str(uuid.uuid4())))
data = {'bar': 'foo'} data = {'bar': 'foo'}

View File

@@ -1,19 +1,19 @@
# pragma pylint: disable=missing-docstring, C0103
# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments
"""
Unit test file for rpc/rpc.py
"""
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock, ANY
import pytest import pytest
from freqtrade import TemporaryError
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc import RPC, RPCException
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap from freqtrade.tests.test_freqtradebot import patch_get_signal
from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange
# Functions for recurrent object patching # Functions for recurrent object patching
@@ -26,21 +26,18 @@ def prec_satoshi(a, b) -> float:
# Unit tests # Unit tests
def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
"""
Test rpc_trade_status() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(), _load_markets=MagicMock(return_value={}),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
@@ -52,50 +49,44 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
rpc._rpc_trade_status() rpc._rpc_trade_status()
freqtradebot.create_trade() freqtradebot.create_trade()
trades = rpc._rpc_trade_status() results = rpc._rpc_trade_status()
trade = trades[0]
result_message = [ assert {
'*Trade ID:* `1`\n' 'trade_id': 1,
'*Current Pair:* ' 'pair': 'ETH/BTC',
'[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'*Open Since:* `just now`\n' 'date': ANY,
'*Amount:* `90.99181074`\n' 'open_rate': 1.099e-05,
'*Open Rate:* `0.00001099`\n' 'close_rate': None,
'*Close Rate:* `None`\n' 'current_rate': 1.098e-05,
'*Current Rate:* `0.00001098`\n' 'amount': 90.99181074,
'*Close Profit:* `None`\n' 'close_profit': None,
'*Current Profit:* `-0.59%`\n' 'current_profit': -0.59,
'*Open Order:* `(limit buy rem=0.00000000)`' 'open_order': '(limit buy rem=0.00000000)'
] } == results[0]
assert trades == result_message
assert trade.find('[ETH/BTC]') >= 0
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
"""
Test rpc_status_table() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'): with pytest.raises(RPCException, match=r'.*trader is not running*'):
rpc._rpc_status_table() rpc._rpc_status_table()
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*\*Status:\* `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_trade()
@@ -107,26 +98,23 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
def test_rpc_daily_profit(default_conf, update, ticker, fee, def test_rpc_daily_profit(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None: limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test rpc_daily_profit() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
stake_currency = default_conf['stake_currency'] stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency'] fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trade()
trade = Trade.query.first() trade = Trade.query.first()
@@ -159,29 +147,28 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None: limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test rpc_trade_statistics() method
"""
patch_get_signal(mocker, (True, False))
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.fiat_convert.Market', 'freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
stake_currency = default_conf['stake_currency'] stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency'] fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
with pytest.raises(RPCException, match=r'.*no closed trade*'): with pytest.raises(RPCException, match=r'.*no closed trade*'):
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
@@ -195,7 +182,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up get_ticker=ticker_sell_up
) )
trade.update(limit_sell_order) trade.update(limit_sell_order)
@@ -210,7 +196,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up get_ticker=ticker_sell_up
) )
trade.update(limit_sell_order) trade.update(limit_sell_order)
@@ -236,10 +221,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
# trade.open_rate (it is set to None) # trade.open_rate (it is set to None)
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
ticker_sell_up, limit_buy_order, limit_sell_order): ticker_sell_up, limit_buy_order, limit_sell_order):
""" patch_exchange(mocker)
Test rpc_trade_statistics() method
"""
patch_get_signal(mocker, (True, False))
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.fiat_convert.Market', 'freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
@@ -248,13 +230,13 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
stake_currency = default_conf['stake_currency'] stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency'] fiat_display_currency = default_conf['fiat_display_currency']
@@ -268,7 +250,6 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
# Update the ticker with a market going up # Update the ticker with a market going up
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up, get_ticker=ticker_sell_up,
get_fee=fee get_fee=fee
) )
@@ -295,9 +276,6 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
def test_rpc_balance_handle(default_conf, mocker): def test_rpc_balance_handle(default_conf, mocker):
"""
Test rpc_balance() method
"""
mock_balance = { mock_balance = {
'BTC': { 'BTC': {
'free': 10.0, 'free': 10.0,
@@ -305,104 +283,101 @@ def test_rpc_balance_handle(default_conf, mocker):
'used': 2.0, 'used': 2.0,
}, },
'ETH': { 'ETH': {
'free': 0.0, 'free': 1.0,
'total': 0.0, 'total': 5.0,
'used': 0.0, 'used': 4.0,
} }
} }
# ETH will be skipped due to mocked Error below
patch_get_signal(mocker, (True, False))
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.fiat_convert.Market', 'freqtrade.fiat_convert.Market',
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(), get_balances=MagicMock(return_value=mock_balance),
get_balances=MagicMock(return_value=mock_balance) get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx'))
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
rpc._fiat_converter = CryptoToFiatConverter()
output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency']) result = rpc._rpc_balance(default_conf['fiat_display_currency'])
assert prec_satoshi(total, 12) assert prec_satoshi(result['total'], 12)
assert prec_satoshi(value, 180000) assert prec_satoshi(result['value'], 180000)
assert 'USD' in symbol assert 'USD' == result['symbol']
assert len(output) == 1 assert result['currencies'] == [{
assert 'BTC' in output[0]['currency'] 'currency': 'BTC',
assert prec_satoshi(output[0]['available'], 10) 'available': 10.0,
assert prec_satoshi(output[0]['balance'], 12) 'balance': 12.0,
assert prec_satoshi(output[0]['pending'], 2) 'pending': 2.0,
assert prec_satoshi(output[0]['est_btc'], 12) 'est_btc': 12.0,
}]
assert result['total'] == 12.0
def test_rpc_start(mocker, default_conf) -> None: def test_rpc_start(mocker, default_conf) -> None:
"""
Test rpc_start() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock() get_ticker=MagicMock()
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
result = rpc._rpc_start() result = rpc._rpc_start()
assert '`Starting trader ...`' in result assert {'status': 'starting trader ...'} == result
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
result = rpc._rpc_start() result = rpc._rpc_start()
assert '*Status:* `already running`' in result assert {'status': 'already running'} == result
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
def test_rpc_stop(mocker, default_conf) -> None: def test_rpc_stop(mocker, default_conf) -> None:
"""
Test rpc_stop() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock() get_ticker=MagicMock()
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
result = rpc._rpc_stop() result = rpc._rpc_stop()
assert '`Stopping trader ...`' in result assert {'status': 'stopping trader ...'} == result
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
result = rpc._rpc_stop() result = rpc._rpc_stop()
assert '*Status:* `already stopped`' in result
assert {'status': 'already stopped'} == result
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
"""
Test rpc_forcesell() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
cancel_order=cancel_order_mock, cancel_order=cancel_order_mock,
get_order=MagicMock( get_order=MagicMock(
@@ -417,14 +392,15 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*`trader is not running`*'): with pytest.raises(RPCException, match=r'.*trader is not running*'):
rpc._rpc_forcesell(None) rpc._rpc_forcesell(None)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
with pytest.raises(RPCException, match=r'.*Invalid argument.*'): with pytest.raises(RPCException, match=r'.*invalid argument*'):
rpc._rpc_forcesell(None) rpc._rpc_forcesell(None)
rpc._rpc_forcesell('all') rpc._rpc_forcesell('all')
@@ -435,10 +411,10 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
rpc._rpc_forcesell('1') rpc._rpc_forcesell('1')
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
with pytest.raises(RPCException, match=r'.*`trader is not running`*'): with pytest.raises(RPCException, match=r'.*trader is not running*'):
rpc._rpc_forcesell(None) rpc._rpc_forcesell(None)
with pytest.raises(RPCException, match=r'.*`trader is not running`*'): with pytest.raises(RPCException, match=r'.*trader is not running*'):
rpc._rpc_forcesell('all') rpc._rpc_forcesell('all')
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
@@ -496,15 +472,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
def test_performance_handle(default_conf, ticker, limit_buy_order, fee, def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, markets, mocker) -> None: limit_sell_order, markets, mocker) -> None:
"""
Test rpc_performance() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker), get_balances=MagicMock(return_value=ticker),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
@@ -512,6 +484,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
# Create some test data # Create some test data
@@ -535,15 +508,11 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
"""
Test rpc_count() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker), get_balances=MagicMock(return_value=ticker),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
@@ -551,6 +520,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
trades = rpc._rpc_count() trades = rpc._rpc_count()

View File

@@ -1,50 +1,31 @@
""" # pragma pylint: disable=missing-docstring, C0103
Unit test file for rpc/rpc_manager.py
"""
import logging import logging
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.rpc.rpc_manager import RPCManager from freqtrade.rpc import RPCMessageType, RPCManager
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
def test_rpc_manager_object() -> None:
""" Test the Arguments object has the mandatory methods """
assert hasattr(RPCManager, 'send_msg')
assert hasattr(RPCManager, 'cleanup')
def test__init__(mocker, default_conf) -> None: def test__init__(mocker, default_conf) -> None:
""" Test __init__() method """ default_conf['telegram']['enabled'] = False
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf)) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
""" Test _init() method with Telegram disabled """
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
default_conf['telegram']['enabled'] = False
conf = deepcopy(default_conf) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
conf['telegram']['enabled'] = False
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
"""
Test _init() method with Telegram enabled
"""
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
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.record_tuples)
@@ -54,16 +35,11 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
"""
Test cleanup() method with Telegram disabled
"""
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
default_conf['telegram']['enabled'] = False
conf = deepcopy(default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.cleanup() rpc_manager.cleanup()
@@ -72,9 +48,6 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
"""
Test cleanup() method with Telegram enabled
"""
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock())
@@ -92,32 +65,51 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
"""
Test send_msg() method with Telegram disabled
"""
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
default_conf['telegram']['enabled'] = False
conf = deepcopy(default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test') rpc_manager.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'test'
})
assert log_has('Sending rpc message: test', caplog.record_tuples) assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
assert telegram_mock.call_count == 0 assert telegram_mock.call_count == 0
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
"""
Test send_msg() method with Telegram disabled
"""
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test') rpc_manager.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'test'
})
assert log_has('Sending rpc message: test', caplog.record_tuples) assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples)
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1
def test_init_webhook_disabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG)
default_conf['telegram']['enabled'] = False
default_conf['webhook'] = {'enabled': False}
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples)
assert rpc_manager.registered_modules == []
def test_init_webhook_enabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG)
default_conf['telegram']['enabled'] = False
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert log_has('Enabling rpc.webhook ...', caplog.record_tuples)
assert len(rpc_manager.registered_modules) == 1
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]

View File

@@ -1,27 +1,27 @@
# pragma pylint: disable=missing-docstring, C0103
# pragma pylint: disable=protected-access, unused-argument, invalid-name # pragma pylint: disable=protected-access, unused-argument, invalid-name
# pragma pylint: disable=too-many-lines, too-many-arguments # pragma pylint: disable=too-many-lines, too-many-arguments
"""
Unit test file for rpc/telegram.py
"""
import re import re
from copy import deepcopy
from datetime import datetime from datetime import datetime
from random import randint from random import randint
from unittest.mock import MagicMock from unittest.mock import MagicMock, ANY
from telegram import Update, Message, Chat import arrow
import pytest
from telegram import Chat, Message, Update
from telegram.error import NetworkError from telegram.error import NetworkError
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.telegram import Telegram from freqtrade.rpc import RPCMessageType
from freqtrade.rpc.telegram import authorized_only from freqtrade.rpc.telegram import Telegram, authorized_only
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import get_patched_freqtradebot, patch_exchange, log_has from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap patch_exchange)
from freqtrade.tests.test_freqtradebot import patch_get_signal
from freqtrade.tests.conftest import patch_coinmarketcap
class DummyCls(Telegram): class DummyCls(Telegram):
@@ -51,9 +51,6 @@ class DummyCls(Telegram):
def test__init__(default_conf, mocker) -> None: def test__init__(default_conf, mocker) -> None:
"""
Test __init__() method
"""
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
@@ -63,7 +60,6 @@ def test__init__(default_conf, mocker) -> None:
def test_init(default_conf, mocker, caplog) -> None: def test_init(default_conf, mocker, caplog) -> None:
""" Test _init() method """
start_polling = MagicMock() start_polling = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
@@ -82,9 +78,6 @@ def test_init(default_conf, mocker, caplog) -> None:
def test_cleanup(default_conf, mocker) -> None: def test_cleanup(default_conf, mocker) -> None:
"""
Test cleanup() method
"""
updater_mock = MagicMock() updater_mock = MagicMock()
updater_mock.stop = MagicMock() updater_mock.stop = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
@@ -95,10 +88,6 @@ def test_cleanup(default_conf, mocker) -> None:
def test_authorized_only(default_conf, mocker, caplog) -> None: def test_authorized_only(default_conf, mocker, caplog) -> None:
"""
Test authorized_only() method when we are authorized
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker, None) patch_exchange(mocker, None)
@@ -106,9 +95,10 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
update = Update(randint(1, 100)) update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf)
dummy = DummyCls(FreqtradeBot(conf)) patch_get_signal(bot, (True, False))
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(
@@ -126,19 +116,16 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
"""
Test authorized_only() method when we are unauthorized
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker, None) patch_exchange(mocker, None)
chat = Chat(0xdeadbeef, 0) chat = Chat(0xdeadbeef, 0)
update = Update(randint(1, 100)) update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf)
dummy = DummyCls(FreqtradeBot(conf)) patch_get_signal(bot, (True, False))
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(
@@ -156,19 +143,18 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
def test_authorized_only_exception(default_conf, mocker, caplog) -> None: def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
"""
Test authorized_only() method when an exception is thrown
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker) patch_exchange(mocker)
update = Update(randint(1, 100)) update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False
dummy = DummyCls(FreqtradeBot(conf)) bot = FreqtradeBot(default_conf)
patch_get_signal(bot, (True, False))
dummy = DummyCls(bot)
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(
@@ -186,19 +172,14 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
"""
Test _status() method
"""
update.message.chat.id = 123 update.message.chat.id = 123
conf = deepcopy(default_conf) default_conf['telegram']['enabled'] = False
conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = 123
conf['telegram']['chat_id'] = 123
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_pair_detail_url=MagicMock(), get_pair_detail_url=MagicMock(),
get_fee=fee, get_fee=fee,
@@ -209,13 +190,26 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
_rpc_trade_status=MagicMock(return_value=[1, 2, 3]), _rpc_trade_status=MagicMock(return_value=[{
'trade_id': 1,
'pair': 'ETH/BTC',
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'date': arrow.utcnow(),
'open_rate': 1.099e-05,
'close_rate': None,
'current_rate': 1.098e-05,
'amount': 90.99181074,
'close_profit': None,
'current_profit': -0.59,
'open_order': '(limit buy rem=0.00000000)'
}]),
_status_table=status_table, _status_table=status_table,
_send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
@@ -223,7 +217,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
freqtradebot.create_trade() freqtradebot.create_trade()
telegram._status(bot=MagicMock(), update=update) telegram._status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 3 assert msg_mock.call_count == 1
update.message.text = MagicMock() update.message.text = MagicMock()
update.message.text.replace = MagicMock(return_value='table 2 3') update.message.text.replace = MagicMock(return_value='table 2 3')
@@ -232,14 +226,10 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None: def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _status() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
@@ -255,6 +245,8 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
@@ -279,14 +271,10 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None: def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _status_table() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value={'id': 'mocked_order_id'}), buy=MagicMock(return_value={'id': 'mocked_order_id'}),
get_fee=fee, get_fee=fee,
@@ -300,9 +288,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
conf = deepcopy(default_conf) default_conf['stake_amount'] = 15.0
conf['stake_amount'] = 15.0 freqtradebot = FreqtradeBot(default_conf)
freqtradebot = FreqtradeBot(conf) patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
@@ -333,18 +322,14 @@ 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:
"""
Test _daily() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
patch_exchange(mocker)
mocker.patch( mocker.patch(
'freqtrade.fiat_convert.CryptoToFiatConverter._find_price', 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
return_value=15000.0 return_value=15000.0
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
@@ -358,6 +343,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
@@ -407,14 +393,10 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
"""
Test _daily() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker get_ticker=ticker
) )
msg_mock = MagicMock() msg_mock = MagicMock()
@@ -426,6 +408,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Try invalid data # Try invalid data
@@ -446,15 +429,11 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None: limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test _profit() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) patch_exchange(mocker)
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
@@ -468,6 +447,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._profit(bot=MagicMock(), update=update) telegram._profit(bot=MagicMock(), update=update)
@@ -507,10 +487,6 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
def test_telegram_balance_handle(default_conf, update, mocker) -> None: def test_telegram_balance_handle(default_conf, update, mocker) -> None:
"""
Test _balance() method
"""
mock_balance = { mock_balance = {
'BTC': { 'BTC': {
'total': 12.0, 'total': 12.0,
@@ -535,9 +511,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
} }
def mock_ticker(symbol, refresh): def mock_ticker(symbol, refresh):
"""
Mock Bittrex.get_ticker() response
"""
if symbol == 'BTC/USDT': if symbol == 'BTC/USDT':
return { return {
'bid': 10000.00, 'bid': 10000.00,
@@ -551,7 +524,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
'last': 0.1, 'last': 0.1,
} }
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance)
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
@@ -564,6 +536,8 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update) telegram._balance(bot=MagicMock(), update=update)
@@ -577,11 +551,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
assert 'BTC: 14.00000000' in result assert 'BTC: 14.00000000' in result
def test_zero_balance_handle(default_conf, update, mocker) -> None: def test_balance_handle_empty_response(default_conf, update, mocker) -> None:
"""
Test _balance() method when the Exchange platform returns nothing
"""
patch_get_signal(mocker, (True, False))
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
msg_mock = MagicMock() msg_mock = MagicMock()
@@ -592,18 +562,17 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
) )
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
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_start_handle(default_conf, update, mocker) -> None: def test_start_handle(default_conf, update, mocker) -> None:
"""
Test _start() method
"""
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
@@ -622,9 +591,6 @@ def test_start_handle(default_conf, update, mocker) -> None:
def test_start_handle_already_running(default_conf, update, mocker) -> None: def test_start_handle_already_running(default_conf, update, mocker) -> None:
"""
Test _start() method
"""
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
@@ -644,9 +610,6 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
def test_stop_handle(default_conf, update, mocker) -> None: def test_stop_handle(default_conf, update, mocker) -> None:
"""
Test _stop() method
"""
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@@ -663,13 +626,10 @@ def test_stop_handle(default_conf, update, mocker) -> None:
telegram._stop(bot=MagicMock(), update=update) telegram._stop(bot=MagicMock(), update=update)
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] assert 'stopping trader' in msg_mock.call_args_list[0][0][0]
def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
"""
Test _stop() method
"""
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@@ -690,7 +650,6 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
def test_reload_conf_handle(default_conf, update, mocker) -> None: def test_reload_conf_handle(default_conf, update, mocker) -> None:
""" Test _reload_conf() method """
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@@ -707,28 +666,25 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
telegram._reload_conf(bot=MagicMock(), update=update) telegram._reload_conf(bot=MagicMock(), update=update)
assert freqtradebot.state == State.RELOAD_CONF assert freqtradebot.state == State.RELOAD_CONF
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Reloading config' in msg_mock.call_args_list[0][0][0] assert 'reloading config' in msg_mock.call_args_list[0][0][0]
def test_forcesell_handle(default_conf, update, ticker, fee, def test_forcesell_handle(default_conf, update, ticker, fee,
ticker_sell_up, markets, mocker) -> None: ticker_sell_up, markets, mocker) -> None:
"""
Test _forcesell() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(), _load_markets=MagicMock(return_value={}),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
@@ -744,33 +700,40 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
assert 'Selling' in rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] assert {
assert 'Amount' in rpc_mock.call_args_list[-1][0][0] 'type': RPCMessageType.SELL_NOTIFICATION,
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] 'exchange': 'Bittrex',
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] 'pair': 'ETH/BTC',
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] 'gain': 'profit',
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'limit': 1.172e-05,
'amount': 90.99181073703367,
'open_rate': 1.099e-05,
'current_rate': 1.172e-05,
'profit_amount': 6.126e-05,
'profit_percent': 0.06110514,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
} == last_msg
def test_forcesell_down_handle(default_conf, update, ticker, fee, def test_forcesell_down_handle(default_conf, update, ticker, fee,
ticker_sell_down, markets, mocker) -> None: ticker_sell_down, markets, mocker) -> None:
"""
Test _forcesell() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(), _load_markets=MagicMock(return_value={}),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
@@ -779,7 +742,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
# Decrease the price and sell it # Decrease the price and sell it
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_down get_ticker=ticker_sell_down
) )
@@ -790,33 +752,41 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert {
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] 'type': RPCMessageType.SELL_NOTIFICATION,
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] 'exchange': 'Bittrex',
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] 'pair': 'ETH/BTC',
'gain': 'loss',
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'limit': 1.044e-05,
'amount': 90.99181073703367,
'open_rate': 1.099e-05,
'current_rate': 1.044e-05,
'profit_amount': -5.492e-05,
'profit_percent': -0.05478343,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
} == last_msg
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _forcesell() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
patch_exchange(mocker)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
@@ -828,17 +798,25 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 4 assert rpc_mock.call_count == 4
for args in rpc_mock.call_args_list: msg = rpc_mock.call_args_list[0][0][0]
assert '0.00001098' in args[0][0] assert {
assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0] 'type': RPCMessageType.SELL_NOTIFICATION,
assert '-0.089 USD' in args[0][0] 'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'gain': 'loss',
'market_url': ANY,
'limit': 1.098e-05,
'amount': 90.99181073703367,
'open_rate': 1.099e-05,
'current_rate': 1.098e-05,
'profit_amount': -5.91e-06,
'profit_percent': -0.00589292,
'stake_currency': 'BTC',
'fiat_currency': 'USD',
} == msg
def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
"""
Test _forcesell() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
msg_mock = MagicMock() msg_mock = MagicMock()
@@ -847,9 +825,10 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
_init=MagicMock(), _init=MagicMock(),
_send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) patch_exchange(mocker)
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Trader is not running # Trader is not running
@@ -865,7 +844,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
update.message.text = '/forcesell' update.message.text = '/forcesell'
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Invalid argument' in msg_mock.call_args_list[0][0][0] assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
# Invalid argument # Invalid argument
msg_mock.reset_mock() msg_mock.reset_mock()
@@ -873,16 +852,13 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
update.message.text = '/forcesell 123456' update.message.text = '/forcesell 123456'
telegram._forcesell(bot=MagicMock(), update=update) telegram._forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0] assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
def test_performance_handle(default_conf, update, ticker, fee, def test_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None: limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test _performance() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
@@ -891,13 +867,13 @@ def test_performance_handle(default_conf, update, ticker, fee,
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_fee=fee, get_fee=fee,
get_markets=markets get_markets=markets
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Create some test data # Create some test data
@@ -920,19 +896,16 @@ def test_performance_handle(default_conf, update, ticker, fee,
def test_performance_handle_invalid(default_conf, update, mocker) -> None: def test_performance_handle_invalid(default_conf, update, mocker) -> None:
"""
Test _performance() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
_send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
# Trader is not running # Trader is not running
@@ -943,11 +916,8 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None: def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _count() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
patch_exchange(mocker)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
@@ -956,13 +926,13 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value={'id': 'mocked_order_id'}), buy=MagicMock(return_value={'id': 'mocked_order_id'}),
get_markets=markets get_markets=markets
) )
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
patch_get_signal(freqtradebot, (True, False))
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
@@ -987,9 +957,6 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
def test_help_handle(default_conf, update, mocker) -> None: def test_help_handle(default_conf, update, mocker) -> None:
"""
Test _help() method
"""
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@@ -1007,9 +974,6 @@ def test_help_handle(default_conf, update, mocker) -> None:
def test_version_handle(default_conf, update, mocker) -> None: def test_version_handle(default_conf, update, mocker) -> None:
"""
Test _version() method
"""
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@@ -1025,15 +989,224 @@ def test_version_handle(default_conf, update, mocker) -> None:
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
def test_send_msg(default_conf, mocker) -> None: def test_send_msg_buy_notification(default_conf, mocker) -> None:
""" msg_mock = MagicMock()
Test send_msg() method mocker.patch.multiple(
""" 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'limit': 1.099e-05,
'stake_amount': 0.001,
'stake_amount_fiat': 0.0,
'stake_currency': 'BTC',
'fiat_currency': 'USD'
})
assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \
'with limit `0.00001099\n' \
'(0.001000 BTC,0.000 USD)`'
def test_send_msg_sell_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
old_convamount = telegram._fiat_converter.convert_amount
telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'gain': 'loss',
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
'limit': 3.201e-05,
'amount': 1333.3333333333335,
'open_rate': 7.5e-05,
'current_rate': 3.201e-05,
'profit_amount': -0.05746268,
'profit_percent': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD'
})
assert msg_mock.call_args[0][0] \
== '*Binance:* Selling [KEY/ETH]' \
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
'*Limit:* `0.00003201`\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00007500`\n' \
'*Current Rate:* `0.00003201`\n' \
'*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`'
msg_mock.reset_mock()
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'gain': 'loss',
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
'limit': 3.201e-05,
'amount': 1333.3333333333335,
'open_rate': 7.5e-05,
'current_rate': 3.201e-05,
'profit_amount': -0.05746268,
'profit_percent': -0.57405275,
'stake_currency': 'ETH',
})
assert msg_mock.call_args[0][0] \
== '*Binance:* Selling [KEY/ETH]' \
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
'*Limit:* `0.00003201`\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00007500`\n' \
'*Current Rate:* `0.00003201`\n' \
'*Profit:* `-57.41%`'
# Reset singleton function to avoid random breaks
telegram._fiat_converter.convert_amount = old_convamount
def test_send_msg_status_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'running'
})
assert msg_mock.call_args[0][0] == '*Status:* `running`'
def test_warning_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.WARNING_NOTIFICATION,
'status': 'message'
})
assert msg_mock.call_args[0][0] == '*Warning:* `message`'
def test_custom_notification(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.CUSTOM_NOTIFICATION,
'status': '*Custom:* `Hello World`'
})
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
def test_send_msg_unknown_type(default_conf, mocker) -> None:
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
with pytest.raises(NotImplementedError, match=r'Unknown message type: None'):
telegram.send_msg({
'type': None,
})
def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
del default_conf['fiat_display_currency']
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
'limit': 1.099e-05,
'stake_amount': 0.001,
'stake_amount_fiat': 0.0,
'stake_currency': 'BTC',
'fiat_currency': None
})
assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \
'with limit `0.00001099\n' \
'(0.001000 BTC)`'
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
del default_conf['fiat_display_currency']
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'gain': 'loss',
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
'limit': 3.201e-05,
'amount': 1333.3333333333335,
'open_rate': 7.5e-05,
'current_rate': 3.201e-05,
'profit_amount': -0.05746268,
'profit_percent': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD'
})
assert msg_mock.call_args[0][0] \
== '*Binance:* Selling [KEY/ETH]' \
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
'*Limit:* `0.00003201`\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00007500`\n' \
'*Current Rate:* `0.00003201`\n' \
'*Profit:* `-57.41%`'
def test__send_msg(default_conf, mocker) -> None:
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
conf = deepcopy(default_conf)
bot = MagicMock() bot = MagicMock()
freqtradebot = get_patched_freqtradebot(mocker, conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
@@ -1041,16 +1214,12 @@ def test_send_msg(default_conf, mocker) -> None:
assert len(bot.method_calls) == 1 assert len(bot.method_calls) == 1
def test_send_msg_network_error(default_conf, mocker, caplog) -> None: def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
"""
Test send_msg() method
"""
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
conf = deepcopy(default_conf)
bot = MagicMock() bot = MagicMock()
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
freqtradebot = get_patched_freqtradebot(mocker, conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True

View File

@@ -0,0 +1,166 @@
# pragma pylint: disable=missing-docstring, C0103, protected-access
from unittest.mock import MagicMock
import pytest
from requests import RequestException
from freqtrade.rpc import RPCMessageType
from freqtrade.rpc.webhook import Webhook
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
def get_webhook_dict() -> dict:
return {
"enabled": True,
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
"webhookbuy": {
"value1": "Buying {pair}",
"value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}"
},
"webhooksell": {
"value1": "Selling {pair}",
"value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency}"
},
"webhookstatus": {
"value1": "Status: {status}",
"value2": "",
"value3": ""
}
}
def test__init__(mocker, default_conf):
default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"}
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
assert webhook._config == default_conf
def test_send_msg(default_conf, mocker):
default_conf["webhook"] = get_webhook_dict()
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
msg = {
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'market_url': "http://mockedurl/ETH_BTC",
'limit': 0.005,
'stake_amount': 0.8,
'stake_amount_fiat': 500,
'stake_currency': 'BTC',
'fiat_currency': 'EUR'
}
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuy"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg))
# Test sell
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg = {
'type': RPCMessageType.SELL_NOTIFICATION,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'gain': "profit",
'market_url': "http://mockedurl/ETH_BTC",
'limit': 0.005,
'amount': 0.8,
'open_rate': 0.004,
'current_rate': 0.005,
'profit_amount': 0.001,
'profit_percent': 0.20,
'stake_currency': 'BTC',
}
webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhooksell"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhooksell"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
# Test notification
msg = {
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'Unfilled sell order for BTC cancelled due to timeout'
}
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
webhook.send_msg(msg)
assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookstatus"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookstatus"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookstatus"]["value3"].format(**msg))
def test_exception_send_msg(default_conf, mocker, caplog):
default_conf["webhook"] = get_webhook_dict()
default_conf["webhook"]["webhookbuy"] = None
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks",
caplog.record_tuples)
default_conf["webhook"] = get_webhook_dict()
default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}"
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
msg = {
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': 'Bittrex',
'pair': 'ETH/BTC',
'market_url': "http://mockedurl/ETH_BTC",
'limit': 0.005,
'stake_amount': 0.8,
'stake_amount_fiat': 500,
'stake_currency': 'BTC',
'fiat_currency': 'EUR'
}
webhook.send_msg(msg)
assert log_has("Problem calling Webhook. Please check your webhook configuration. "
"Exception: 'DEADBEEF'", caplog.record_tuples)
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg = {
'type': 'DEADBEEF',
'status': 'whatever'
}
with pytest.raises(NotImplementedError):
webhook.send_msg(msg)
def test__send_msg(default_conf, mocker, caplog):
default_conf["webhook"] = get_webhook_dict()
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
msg = {'value1': 'DEADBEEF',
'value2': 'ALIVEBEEF',
'value3': 'FREQTRADE'}
post = MagicMock()
mocker.patch("freqtrade.rpc.webhook.post", post)
webhook._send_msg(msg)
assert post.call_count == 1
assert post.call_args[1] == {'data': msg}
assert post.call_args[0] == (default_conf['webhook']['url'], )
post = MagicMock(side_effect=RequestException)
mocker.patch("freqtrade.rpc.webhook.post", post)
webhook._send_msg(msg)
assert log_has('Could not call webhook url. Exception: ', caplog.record_tuples)

View File

@@ -0,0 +1,235 @@
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy # noqa
# This class is a sample. Feel free to customize it.
class TestStrategyLegacy(IStrategy):
"""
This is a test strategy using the legacy function headers, which will be
removed in a future update.
Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py
for a uptodate version of this template.
"""
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.10
# Optimal ticker interval for the strategy
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
"""
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
"""
# Awesome oscillator
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
dataframe['cci'] = ta.CCI(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# Minus Directional Indicator / Movement
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# ROC
dataframe['roc'] = ta.ROC(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
"""
# Overlap Studies
# ------------------------------------
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
"""
# EMA - Exponential Moving Average
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
"""
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
"""
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
"""
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
"""
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 30) &
(dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1))
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 70) &
(dataframe['tema'] > dataframe['bb_middleband']) &
(dataframe['tema'] < dataframe['tema'].shift(1))
),
'sell'] = 1
return dataframe

View File

@@ -3,14 +3,14 @@ import json
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.analyze import Analyze from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
from freqtrade.strategy.default_strategy import DefaultStrategy from freqtrade.strategy.default_strategy import DefaultStrategy
@pytest.fixture @pytest.fixture
def result(): def result():
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file: with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
return Analyze.parse_ticker_dataframe(json.load(data_file)) return parse_ticker_dataframe(json.load(data_file))
def test_default_strategy_structure(): def test_default_strategy_structure():
@@ -23,12 +23,13 @@ def test_default_strategy_structure():
def test_default_strategy(result): def test_default_strategy(result):
strategy = DefaultStrategy() strategy = DefaultStrategy({})
metadata = {'pair': 'ETH/BTC'}
assert type(strategy.minimal_roi) is dict assert type(strategy.minimal_roi) is dict
assert type(strategy.stoploss) is float assert type(strategy.stoploss) is float
assert type(strategy.ticker_interval) is str assert type(strategy.ticker_interval) is str
indicators = strategy.populate_indicators(result) indicators = strategy.populate_indicators(result, metadata)
assert type(indicators) is DataFrame assert type(indicators) is DataFrame
assert type(strategy.populate_buy_trend(indicators)) is DataFrame assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame
assert type(strategy.populate_sell_trend(indicators)) is DataFrame assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame

View File

@@ -0,0 +1,202 @@
# pragma pylint: disable=missing-docstring, C0103
import logging
from unittest.mock import MagicMock
import arrow
from pandas import DataFrame
from freqtrade.arguments import TimeRange
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.persistence import Trade
from freqtrade.tests.conftest import get_patched_exchange, log_has
from freqtrade.strategy.default_strategy import DefaultStrategy
# Avoid to reinit the same object again and again
_STRATEGY = DefaultStrategy(config={})
def test_returns_latest_buy_signal(mocker, default_conf):
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False)
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True)
def test_returns_latest_sell_signal(mocker, default_conf):
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True)
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False)
def test_get_signal_empty(default_conf, mocker, caplog):
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
None)
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
side_effect=ValueError('xyz')
)
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], 1)
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
return_value=DataFrame([])
)
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1)
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
def test_get_signal_old_dataframe(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
# default_conf defines a 5m interval. we check interval * 2 + 5m
# this is necessary as the last candle is removed (partial candles) by default
oldtime = arrow.utcnow().shift(minutes=-16)
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
return_value=DataFrame(ticks)
)
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1)
assert log_has(
'Outdated history for pair xyz. Last tick is 16 minutes old',
caplog.record_tuples
)
def test_get_signal_handles_exceptions(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
side_effect=Exception('invalid ticker history ')
)
assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
def test_tickerdata_to_dataframe(default_conf) -> None:
strategy = DefaultStrategy(default_conf)
timerange = TimeRange(None, 'line', 0, -100)
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': tick}
data = strategy.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
def test_min_roi_reached(default_conf, fee) -> None:
strategy = DefaultStrategy(default_conf)
strategy.minimal_roi = {0: 0.1, 20: 0.05, 55: 0.01}
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.01, arrow.utcnow().shift(minutes=-55).datetime)
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-55).datetime)
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
assert not strategy.min_roi_reached(trade, -0.01, arrow.utcnow().shift(minutes=-1).datetime)
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x)
sell_mock = MagicMock(side_effect=lambda x, meta: x)
mocker.patch.multiple(
'freqtrade.strategy.interface.IStrategy',
advise_indicators=ind_mock,
advise_buy=buy_mock,
advise_sell=sell_mock,
)
strategy = DefaultStrategy({})
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
assert ind_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 not log_has('Skippinig TA Analysis for already analyzed candle',
caplog.record_tuples)
caplog.clear()
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true
assert ind_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 not log_has('Skippinig TA Analysis for already analyzed candle',
caplog.record_tuples)
def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
caplog.set_level(logging.DEBUG)
ind_mock = MagicMock(side_effect=lambda x, meta: x)
buy_mock = MagicMock(side_effect=lambda x, meta: x)
sell_mock = MagicMock(side_effect=lambda x, meta: x)
mocker.patch.multiple(
'freqtrade.strategy.interface.IStrategy',
advise_indicators=ind_mock,
advise_buy=buy_mock,
advise_sell=sell_mock,
)
strategy = DefaultStrategy({})
strategy.process_only_new_candles = True
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
assert ind_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 not log_has('Skippinig TA Analysis for already analyzed candle',
caplog.record_tuples)
caplog.clear()
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
# No analysis happens as process_only_new_candles is true
assert ind_mock.call_count == 1
assert buy_mock.call_count == 1
assert buy_mock.call_count == 1
# only skipped analyze adds buy and sell columns, otherwise it's all mocked
assert 'buy' in ret
assert 'sell' in ret
assert ret['buy'].sum() == 0
assert ret['sell'].sum() == 0
assert not log_has('TA Analysis Launched', caplog.record_tuples)
assert log_has('Skippinig TA Analysis for already analyzed candle',
caplog.record_tuples)

View File

@@ -1,8 +1,11 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103 # pragma pylint: disable=missing-docstring, protected-access, C0103
import logging import logging
import os from base64 import urlsafe_b64encode
from os import path
import warnings
import pytest import pytest
from pandas import DataFrame
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
@@ -12,14 +15,15 @@ from freqtrade.strategy.resolver import StrategyResolver
def test_import_strategy(caplog): def test_import_strategy(caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
default_config = {}
strategy = DefaultStrategy() strategy = DefaultStrategy(default_config)
strategy.some_method = lambda *args, **kwargs: 42 strategy.some_method = lambda *args, **kwargs: 42
assert strategy.__module__ == 'freqtrade.strategy.default_strategy' assert strategy.__module__ == 'freqtrade.strategy.default_strategy'
assert strategy.some_method() == 42 assert strategy.some_method() == 42
imported_strategy = import_strategy(strategy) imported_strategy = import_strategy(strategy, default_config)
assert dir(strategy) == dir(imported_strategy) assert dir(strategy) == dir(imported_strategy)
@@ -35,25 +39,42 @@ def test_import_strategy(caplog):
def test_search_strategy(): def test_search_strategy():
default_location = os.path.join(os.path.dirname( default_config = {}
os.path.realpath(__file__)), '..', '..', 'strategy' default_location = path.join(path.dirname(
path.realpath(__file__)), '..', '..', 'strategy'
) )
assert isinstance( assert isinstance(
StrategyResolver._search_strategy(default_location, 'DefaultStrategy'), IStrategy StrategyResolver._search_strategy(
default_location,
config=default_config,
strategy_name='DefaultStrategy'
),
IStrategy
) )
assert StrategyResolver._search_strategy(default_location, 'NotFoundStrategy') is None assert StrategyResolver._search_strategy(
default_location,
config=default_config,
strategy_name='NotFoundStrategy'
) is None
def test_load_strategy(result): def test_load_strategy(result):
resolver = StrategyResolver({'strategy': 'TestStrategy'}) resolver = StrategyResolver({'strategy': 'TestStrategy'})
assert hasattr(resolver.strategy, 'populate_indicators') metadata = {'pair': 'ETH/BTC'}
assert 'adx' in resolver.strategy.populate_indicators(result) assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata)
def test_load_strategy_byte64(result):
with open("freqtrade/tests/strategy/test_strategy.py", "r") as file:
encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8")
resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)})
assert 'adx' in resolver.strategy.advise_indicators(result, 'ETH/BTC')
def test_load_strategy_invalid_directory(result, caplog): def test_load_strategy_invalid_directory(result, caplog):
resolver = StrategyResolver() resolver = StrategyResolver()
extra_dir = os.path.join('some', 'path') extra_dir = path.join('some', 'path')
resolver._load_strategy('TestStrategy', extra_dir) resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir)
assert ( assert (
'freqtrade.strategy.resolver', 'freqtrade.strategy.resolver',
@@ -61,8 +82,7 @@ def test_load_strategy_invalid_directory(result, caplog):
'Path "{}" does not exist'.format(extra_dir), 'Path "{}" does not exist'.format(extra_dir),
) in caplog.record_tuples ) in caplog.record_tuples
assert hasattr(resolver.strategy, 'populate_indicators') assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'})
assert 'adx' in resolver.strategy.populate_indicators(result)
def test_load_not_found_strategy(): def test_load_not_found_strategy():
@@ -70,27 +90,30 @@ def test_load_not_found_strategy():
with pytest.raises(ImportError, with pytest.raises(ImportError,
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('NotFoundStrategy') strategy._load_strategy(strategy_name='NotFoundStrategy', config={})
def test_strategy(result): def test_strategy(result):
resolver = StrategyResolver({'strategy': 'DefaultStrategy'}) config = {'strategy': 'DefaultStrategy'}
assert hasattr(resolver.strategy, 'minimal_roi') resolver = StrategyResolver(config)
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 hasattr(resolver.strategy, 'stoploss')
assert resolver.strategy.stoploss == -0.10 assert resolver.strategy.stoploss == -0.10
assert config['stoploss'] == -0.10
assert hasattr(resolver.strategy, 'populate_indicators') assert resolver.strategy.ticker_interval == '5m'
assert 'adx' in resolver.strategy.populate_indicators(result) assert config['ticker_interval'] == '5m'
assert hasattr(resolver.strategy, 'populate_buy_trend') df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata)
dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result)) assert 'adx' in df_indicators
dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata)
assert 'buy' in dataframe.columns assert 'buy' in dataframe.columns
assert hasattr(resolver.strategy, 'populate_sell_trend') dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata)
dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result))
assert 'sell' in dataframe.columns assert 'sell' in dataframe.columns
@@ -104,11 +127,10 @@ def test_strategy_override_minimal_roi(caplog):
} }
resolver = StrategyResolver(config) resolver = StrategyResolver(config)
assert hasattr(resolver.strategy, 'minimal_roi')
assert resolver.strategy.minimal_roi[0] == 0.5 assert resolver.strategy.minimal_roi[0] == 0.5
assert ('freqtrade.strategy.resolver', assert ('freqtrade.strategy.resolver',
logging.INFO, logging.INFO,
'Override strategy \'minimal_roi\' with value in config file.' "Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
) in caplog.record_tuples ) in caplog.record_tuples
@@ -120,11 +142,10 @@ def test_strategy_override_stoploss(caplog):
} }
resolver = StrategyResolver(config) resolver = StrategyResolver(config)
assert hasattr(resolver.strategy, 'stoploss')
assert resolver.strategy.stoploss == -0.5 assert resolver.strategy.stoploss == -0.5
assert ('freqtrade.strategy.resolver', assert ('freqtrade.strategy.resolver',
logging.INFO, logging.INFO,
'Override strategy \'stoploss\' with value in config file: -0.5.' "Override strategy 'stoploss' with value in config file: -0.5."
) in caplog.record_tuples ) in caplog.record_tuples
@@ -137,9 +158,81 @@ def test_strategy_override_ticker_interval(caplog):
} }
resolver = StrategyResolver(config) resolver = StrategyResolver(config)
assert hasattr(resolver.strategy, 'ticker_interval')
assert resolver.strategy.ticker_interval == 60 assert resolver.strategy.ticker_interval == 60
assert ('freqtrade.strategy.resolver', assert ('freqtrade.strategy.resolver',
logging.INFO, logging.INFO,
'Override strategy \'ticker_interval\' with value in config file: 60.' "Override strategy 'ticker_interval' with value in config file: 60."
) in caplog.record_tuples ) in caplog.record_tuples
def test_strategy_override_process_only_new_candles(caplog):
caplog.set_level(logging.INFO)
config = {
'strategy': 'DefaultStrategy',
'process_only_new_candles': True
}
resolver = StrategyResolver(config)
assert resolver.strategy.process_only_new_candles
assert ('freqtrade.strategy.resolver',
logging.INFO,
"Override process_only_new_candles 'process_only_new_candles' "
"with value in config file: True."
) in caplog.record_tuples
def test_deprecate_populate_indicators(result):
default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location})
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC')
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \
in str(w[-1].message)
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
resolver.strategy.advise_buy(indicators, 'ETH/BTC')
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \
in str(w[-1].message)
with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
resolver.strategy.advise_sell(indicators, 'ETH_BTC')
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated - check out the Sample strategy to see the current function headers!" \
in str(w[-1].message)
def test_call_deprecated_function(result, monkeypatch):
default_location = path.join(path.dirname(path.realpath(__file__)))
resolver = StrategyResolver({'strategy': 'TestStrategyLegacy',
'strategy_path': default_location})
metadata = {'pair': 'ETH/BTC'}
# Make sure we are using a legacy function
assert resolver.strategy._populate_fun_len == 2
assert resolver.strategy._buy_fun_len == 2
assert resolver.strategy._sell_fun_len == 2
indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata)
assert type(indicator_df) is DataFrame
assert 'adx' in indicator_df.columns
buydf = resolver.strategy.advise_buy(result, metadata=metadata)
assert type(buydf) is DataFrame
assert 'buy' in buydf.columns
selldf = resolver.strategy.advise_sell(result, metadata=metadata)
assert type(selldf) is DataFrame
assert 'sell' in selldf

View File

@@ -1,40 +1,40 @@
# pragma pylint: disable=missing-docstring,C0103,protected-access # pragma pylint: disable=missing-docstring,C0103,protected-access
import freqtrade.tests.conftest as tt # test tools
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.tests.conftest import get_patched_freqtradebot
import pytest
# whitelist, blacklist, filtering, all of that will # whitelist, blacklist, filtering, all of that will
# eventually become some rules to run on a generic ACL engine # eventually become some rules to run on a generic ACL engine
# perhaps try to anticipate that by using some python package # perhaps try to anticipate that by using some python package
def whitelist_conf(): @pytest.fixture(scope="function")
config = tt.default_conf() def whitelist_conf(default_conf):
default_conf['stake_currency'] = 'BTC'
config['stake_currency'] = 'BTC' default_conf['exchange']['pair_whitelist'] = [
config['exchange']['pair_whitelist'] = [
'ETH/BTC', 'ETH/BTC',
'TKN/BTC', 'TKN/BTC',
'TRST/BTC', 'TRST/BTC',
'SWT/BTC', 'SWT/BTC',
'BCC/BTC' 'BCC/BTC'
] ]
default_conf['exchange']['pair_blacklist'] = [
config['exchange']['pair_blacklist'] = [
'BLK/BTC' 'BLK/BTC'
] ]
return config return default_conf
def test_refresh_market_pair_not_in_whitelist(mocker, markets): def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf):
conf = whitelist_conf()
freqtradebot = tt.get_patched_freqtradebot(mocker, conf) freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
refreshedwhitelist = freqtradebot._refresh_whitelist( refreshedwhitelist = freqtradebot._refresh_whitelist(
conf['exchange']['pair_whitelist'] + ['XXX/BTC'] whitelist_conf['exchange']['pair_whitelist'] + ['XXX/BTC']
) )
# List ordered by BaseVolume # List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC'] whitelist = ['ETH/BTC', 'TKN/BTC']
@@ -42,12 +42,12 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets):
assert whitelist == refreshedwhitelist assert whitelist == refreshedwhitelist
def test_refresh_whitelist(mocker, markets): def test_refresh_whitelist(mocker, markets, whitelist_conf):
conf = whitelist_conf() freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist']) refreshedwhitelist = freqtradebot._refresh_whitelist(
whitelist_conf['exchange']['pair_whitelist'])
# List ordered by BaseVolume # List ordered by BaseVolume
whitelist = ['ETH/BTC', 'TKN/BTC'] whitelist = ['ETH/BTC', 'TKN/BTC']
@@ -55,9 +55,8 @@ def test_refresh_whitelist(mocker, markets):
assert whitelist == refreshedwhitelist assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic(mocker, markets, tickers): def test_refresh_whitelist_dynamic(mocker, markets, tickers, whitelist_conf):
conf = whitelist_conf() freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_markets=markets, get_markets=markets,
@@ -69,21 +68,20 @@ def test_refresh_whitelist_dynamic(mocker, markets, tickers):
whitelist = ['ETH/BTC', 'TKN/BTC'] whitelist = ['ETH/BTC', 'TKN/BTC']
refreshedwhitelist = freqtradebot._refresh_whitelist( refreshedwhitelist = freqtradebot._refresh_whitelist(
freqtradebot._gen_pair_whitelist(conf['stake_currency']) freqtradebot._gen_pair_whitelist(whitelist_conf['stake_currency'])
) )
assert whitelist == refreshedwhitelist assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty): def test_refresh_whitelist_dynamic_empty(mocker, markets_empty, whitelist_conf):
conf = whitelist_conf() freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty) mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty)
# argument: use the whitelist dynamically by exchange-volume # argument: use the whitelist dynamically by exchange-volume
whitelist = [] whitelist = []
conf['exchange']['pair_whitelist'] = [] whitelist_conf['exchange']['pair_whitelist'] = []
freqtradebot._refresh_whitelist(whitelist) freqtradebot._refresh_whitelist(whitelist)
pairslist = conf['exchange']['pair_whitelist'] pairslist = whitelist_conf['exchange']['pair_whitelist']
assert set(whitelist) == set(pairslist) assert set(whitelist) == set(pairslist)

View File

@@ -1,197 +0,0 @@
# pragma pylint: disable=missing-docstring, C0103
"""
Unit test file for analyse.py
"""
import datetime
import logging
from unittest.mock import MagicMock
import arrow
from pandas import DataFrame
from freqtrade.analyze import Analyze, SignalType
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.arguments import TimeRange
from freqtrade.tests.conftest import log_has, get_patched_exchange
# Avoid to reinit the same object again and again
_ANALYZE = Analyze({'strategy': 'DefaultStrategy'})
def test_signaltype_object() -> None:
"""
Test the SignalType object has the mandatory Constants
:return: None
"""
assert hasattr(SignalType, 'BUY')
assert hasattr(SignalType, 'SELL')
def test_analyze_object() -> None:
"""
Test the Analyze object has the mandatory methods
:return: None
"""
assert hasattr(Analyze, 'parse_ticker_dataframe')
assert hasattr(Analyze, 'populate_indicators')
assert hasattr(Analyze, 'populate_buy_trend')
assert hasattr(Analyze, 'populate_sell_trend')
assert hasattr(Analyze, 'analyze_ticker')
assert hasattr(Analyze, 'get_signal')
assert hasattr(Analyze, 'should_sell')
assert hasattr(Analyze, 'min_roi_reached')
def test_dataframe_correct_length(result):
dataframe = Analyze.parse_ticker_dataframe(result)
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
def test_dataframe_correct_columns(result):
assert result.columns.tolist() == \
['date', 'open', 'high', 'low', 'close', 'volume']
def test_populates_buy_trend(result):
# Load the default strategy for the unit test, because this logic is done in main.py
dataframe = _ANALYZE.populate_buy_trend(_ANALYZE.populate_indicators(result))
assert 'buy' in dataframe.columns
def test_populates_sell_trend(result):
# Load the default strategy for the unit test, because this logic is done in main.py
dataframe = _ANALYZE.populate_sell_trend(_ANALYZE.populate_indicators(result))
assert 'sell' in dataframe.columns
def test_returns_latest_buy_signal(mocker, default_conf):
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
def test_returns_latest_sell_signal(mocker, default_conf):
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False)
def test_get_signal_empty(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=None)
exchange = get_patched_exchange(mocker, default_conf)
assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval'])
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
side_effect=ValueError('xyz')
)
)
assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval'])
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame([])
)
)
assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval'])
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
def test_get_signal_old_dataframe(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1)
exchange = get_patched_exchange(mocker, default_conf)
# FIX: The get_signal function has hardcoded 10, which we must inturn hardcode
oldtime = arrow.utcnow() - datetime.timedelta(minutes=11)
ticks = DataFrame([{'buy': 1, 'date': oldtime}])
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
return_value=DataFrame(ticks)
)
)
assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval'])
assert log_has(
'Outdated history for pair xyz. Last tick is 11 minutes old',
caplog.record_tuples
)
def test_get_signal_handles_exceptions(mocker, default_conf):
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock())
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
analyze_ticker=MagicMock(
side_effect=Exception('invalid ticker history ')
)
)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
def test_parse_ticker_dataframe(ticker_history):
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
# Test file with BV data
dataframe = Analyze.parse_ticker_dataframe(ticker_history)
assert dataframe.columns.tolist() == columns
def test_tickerdata_to_dataframe(default_conf) -> None:
"""
Test Analyze.tickerdata_to_dataframe() method
"""
analyze = Analyze(default_conf)
timerange = TimeRange(None, 'line', 0, -100)
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': tick}
data = analyze.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed

View File

@@ -1,41 +1,24 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
"""
Unit test file for arguments.py
"""
import argparse import argparse
import logging
import pytest import pytest
from freqtrade.arguments import Arguments, TimeRange from freqtrade.arguments import Arguments, TimeRange
def test_arguments_object() -> None:
"""
Test the Arguments object has the mandatory methods
:return: None
"""
assert hasattr(Arguments, 'get_parsed_arg')
assert hasattr(Arguments, 'parse_args')
assert hasattr(Arguments, 'parse_timerange')
assert hasattr(Arguments, 'scripts_options')
# Parse common command-line-arguments. Used for all tools # Parse common command-line-arguments. Used for all tools
def test_parse_args_none() -> None: def test_parse_args_none() -> None:
arguments = Arguments([], '') arguments = Arguments([], '')
assert isinstance(arguments, Arguments) assert isinstance(arguments, Arguments)
assert isinstance(arguments.parser, argparse.ArgumentParser) assert isinstance(arguments.parser, argparse.ArgumentParser)
assert isinstance(arguments.parser, argparse.ArgumentParser)
def test_parse_args_defaults() -> None: def test_parse_args_defaults() -> None:
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
assert args.config == 'config.json' assert args.config == 'config.json'
assert args.dynamic_whitelist is None assert args.dynamic_whitelist is None
assert args.loglevel == logging.INFO assert args.loglevel == 0
def test_parse_args_config() -> None: def test_parse_args_config() -> None:
@@ -53,10 +36,10 @@ 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 == logging.DEBUG assert args.loglevel == 1
args = Arguments(['--verbose'], '').get_parsed_arg() args = Arguments(['--verbose'], '').get_parsed_arg()
assert args.loglevel == logging.DEBUG assert args.loglevel == 1
def test_scripts_options() -> None: def test_scripts_options() -> None:
@@ -149,15 +132,21 @@ def test_parse_args_backtesting_custom() -> None:
'backtesting', 'backtesting',
'--live', '--live',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--refresh-pairs-cached'] '--refresh-pairs-cached',
'--strategy-list',
'DefaultStrategy',
'TestStrategy'
]
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.live is True
assert call_args.loglevel == logging.INFO 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'
assert call_args.refresh_pairs is True assert call_args.refresh_pairs is True
assert type(call_args.strategy_list) is list
assert len(call_args.strategy_list) == 2
def test_parse_args_hyperopt_custom() -> None: def test_parse_args_hyperopt_custom() -> None:
@@ -170,7 +159,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 == logging.INFO assert call_args.loglevel == 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

View File

@@ -1,76 +1,46 @@
# pragma pylint: disable=protected-access, invalid-name # pragma pylint: disable=missing-docstring, protected-access, invalid-name
"""
Unit test file for configuration.py
"""
import json import json
from copy import deepcopy
from unittest.mock import MagicMock
from argparse import Namespace from argparse import Namespace
import logging
from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import ValidationError from jsonschema import validate, ValidationError
from freqtrade.arguments import Arguments from freqtrade import constants
from freqtrade.configuration import Configuration
from freqtrade.constants import DEFAULT_DB_PROD_URL, DEFAULT_DB_DRYRUN_URL
from freqtrade.tests.conftest import log_has
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration, set_loggers
def test_configuration_object() -> None: from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
""" from freqtrade.tests.conftest import log_has
Test the Constants object has the mandatory Constants
"""
assert hasattr(Configuration, 'load_config')
assert hasattr(Configuration, '_load_config_file')
assert hasattr(Configuration, '_validate_config')
assert hasattr(Configuration, '_load_common_config')
assert hasattr(Configuration, '_load_backtesting_config')
assert hasattr(Configuration, '_load_hyperopt_config')
assert hasattr(Configuration, 'get_config')
def test_load_config_invalid_pair(default_conf) -> None: def test_load_config_invalid_pair(default_conf) -> None:
""" default_conf['exchange']['pair_whitelist'].append('ETH-BTC')
Test the configuration validator with an invalid PAIR format
"""
conf = deepcopy(default_conf)
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()) configuration = Configuration(Namespace())
configuration._validate_config(conf) configuration._validate_config(default_conf)
def test_load_config_missing_attributes(default_conf) -> None: def test_load_config_missing_attributes(default_conf) -> None:
""" default_conf.pop('exchange')
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
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()) configuration = Configuration(Namespace())
configuration._validate_config(conf) configuration._validate_config(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'
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
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()) configuration = Configuration(Namespace())
configuration._validate_config(conf) configuration._validate_config(default_conf)
def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_file(default_conf, mocker, caplog) -> None:
"""
Test Configuration._load_config_file() method
"""
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@@ -84,13 +54,9 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
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
Test Configuration._load_config_file() method
"""
conf = deepcopy(default_conf)
conf['max_open_trades'] = 0
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf) read_data=json.dumps(default_conf)
)) ))
Configuration(Namespace())._load_config_file('somefile') Configuration(Namespace())._load_config_file('somefile')
@@ -99,9 +65,6 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
def test_load_config_file_exception(mocker) -> None: def test_load_config_file_exception(mocker) -> None:
"""
Test Configuration._load_config_file() method
"""
mocker.patch( mocker.patch(
'freqtrade.configuration.open', 'freqtrade.configuration.open',
MagicMock(side_effect=FileNotFoundError('File not found')) MagicMock(side_effect=FileNotFoundError('File not found'))
@@ -113,9 +76,6 @@ def test_load_config_file_exception(mocker) -> None:
def test_load_config(default_conf, mocker) -> None: def test_load_config(default_conf, mocker) -> None:
"""
Test Configuration.load_config() without any cli params
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@@ -130,13 +90,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:
"""
Test Configuration.load_config() with cli params used
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
arglist = [ arglist = [
'--dynamic-whitelist', '10', '--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@@ -144,7 +100,6 @@ def test_load_config_with_params(default_conf, mocker) -> None:
'--db-url', 'sqlite:///someurl', '--db-url', 'sqlite:///someurl',
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@@ -161,10 +116,10 @@ def test_load_config_with_params(default_conf, mocker) -> None:
)) ))
arglist = [ arglist = [
'--dynamic-whitelist', '10', '--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path' '--strategy-path', '/some/path'
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@@ -192,16 +147,12 @@ def test_load_config_with_params(default_conf, mocker) -> None:
def test_load_custom_strategy(default_conf, mocker) -> None: def test_load_custom_strategy(default_conf, mocker) -> None:
""" default_conf.update({
Test Configuration.load_config() without any cli params
"""
custom_conf = deepcopy(default_conf)
custom_conf.update({
'strategy': 'CustomStrategy', 'strategy': 'CustomStrategy',
'strategy_path': '/tmp/strategies', 'strategy_path': '/tmp/strategies',
}) })
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(custom_conf) read_data=json.dumps(default_conf)
)) ))
args = Arguments([], '').get_parsed_arg() args = Arguments([], '').get_parsed_arg()
@@ -213,13 +164,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:
"""
Test Configuration.show_info()
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
arglist = [ arglist = [
'--dynamic-whitelist', '10', '--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
@@ -236,19 +183,14 @@ def test_show_info(default_conf, mocker, caplog) -> None:
'(not applicable with Backtesting and Hyperopt)', '(not applicable with Backtesting and Hyperopt)',
caplog.record_tuples caplog.record_tuples
) )
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples) assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
assert log_has('Dry run is enabled', 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:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
arglist = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
@@ -275,8 +217,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'live' not in config assert 'live' not in config
assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation' not in config assert 'position_stacking' not in config
assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples) assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
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.record_tuples)
@@ -286,9 +228,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
mocker.patch('freqtrade.configuration.open', mocker.mock_open( mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
@@ -300,7 +239,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'backtesting', 'backtesting',
'--ticker-interval', '1m', '--ticker-interval', '1m',
'--live', '--live',
'--realistic-simulation', '--enable-position-stacking',
'--disable-max-market-positions',
'--refresh-pairs-cached', '--refresh-pairs-cached',
'--timerange', ':100', '--timerange', ':100',
'--export', '/bar/foo' '--export', '/bar/foo'
@@ -330,9 +270,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'live' in config assert 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples) assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation'in config assert 'position_stacking'in config
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples) assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples)
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
assert 'use_max_market_positions' in config
assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples)
assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples)
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.record_tuples)
@@ -349,7 +292,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
) )
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None:
""" """
Test setup_configuration() function Test setup_configuration() function
""" """
@@ -357,12 +300,62 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
arglist = [
'--config', 'config.json',
'backtesting',
'--ticker-interval', '1m',
'--export', '/bar/foo',
'--strategy-list',
'DefaultStrategy',
'TestStrategy'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
assert 'max_open_trades' in config
assert 'stake_currency' in config
assert 'stake_amount' in config
assert 'exchange' in config
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert log_has(
'Using ticker_interval: 1m ...',
caplog.record_tuples
)
assert 'strategy_list' in config
assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples)
assert 'position_stacking' not in config
assert 'use_max_market_positions' not in config
assert 'timerange' not in config
assert 'export' in config
assert log_has(
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
arglist = [ arglist = [
'hyperopt', 'hyperopt',
'--epochs', '10', '--epochs', '10',
'--spaces', 'all', '--spaces', 'all',
] ]
args = Arguments(arglist, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
@@ -379,26 +372,79 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
def test_check_exchange(default_conf) -> None: def test_check_exchange(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
configuration = Configuration(Namespace()) configuration = Configuration(Namespace())
# Test a valid exchange # Test a valid exchange
conf.get('exchange').update({'name': 'BITTREX'}) default_conf.get('exchange').update({'name': 'BITTREX'})
assert configuration.check_exchange(conf) assert configuration.check_exchange(default_conf)
# Test a valid exchange # Test a valid exchange
conf.get('exchange').update({'name': 'binance'}) default_conf.get('exchange').update({'name': 'binance'})
assert configuration.check_exchange(conf) assert configuration.check_exchange(default_conf)
# Test a invalid exchange # Test a invalid exchange
conf.get('exchange').update({'name': 'unknown_exchange'}) default_conf.get('exchange').update({'name': 'unknown_exchange'})
configuration.config = conf configuration.config = default_conf
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
match=r'.*Exchange "unknown_exchange" not supported.*' match=r'.*Exchange "unknown_exchange" not supported.*'
): ):
configuration.check_exchange(conf) configuration.check_exchange(default_conf)
def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None:
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)))
# Prevent setting loggers
mocker.patch('freqtrade.configuration.set_loggers', MagicMock)
arglist = ['-vvv']
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('verbosity') == 3
assert log_has('Verbosity set to 3', caplog.record_tuples)
def test_set_loggers() -> None:
# Reset Logging to Debug, otherwise this fails randomly as it's set globally
logging.getLogger('requests').setLevel(logging.DEBUG)
logging.getLogger("urllib3").setLevel(logging.DEBUG)
logging.getLogger('ccxt.base.exchange').setLevel(logging.DEBUG)
logging.getLogger('telegram').setLevel(logging.DEBUG)
previous_value1 = logging.getLogger('requests').level
previous_value2 = logging.getLogger('ccxt.base.exchange').level
previous_value3 = logging.getLogger('telegram').level
set_loggers()
value1 = logging.getLogger('requests').level
assert previous_value1 is not value1
assert value1 is logging.INFO
value2 = logging.getLogger('ccxt.base.exchange').level
assert previous_value2 is not value2
assert value2 is logging.INFO
value3 = logging.getLogger('telegram').level
assert previous_value3 is not value3
assert value3 is logging.INFO
set_loggers(log_level=2)
assert logging.getLogger('requests').level is logging.DEBUG
assert logging.getLogger('ccxt.base.exchange').level is logging.INFO
assert logging.getLogger('telegram').level is logging.INFO
set_loggers(log_level=3)
assert logging.getLogger('requests').level is logging.DEBUG
assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG
assert logging.getLogger('telegram').level is logging.INFO
def test_validate_default_conf(default_conf) -> None:
validate(default_conf, constants.CONF_SCHEMA)

View File

@@ -1,25 +0,0 @@
"""
Unit test file for constants.py
"""
from freqtrade import constants
def test_constant_object() -> None:
"""
Test the Constants object has the mandatory Constants
"""
assert hasattr(constants, 'CONF_SCHEMA')
assert hasattr(constants, 'DYNAMIC_WHITELIST')
assert hasattr(constants, 'PROCESS_THROTTLE_SECS')
assert hasattr(constants, 'TICKER_INTERVAL')
assert hasattr(constants, 'HYPEROPT_EPOCH')
assert hasattr(constants, 'RETRY_TIMEOUT')
assert hasattr(constants, 'DEFAULT_STRATEGY')
def test_conf_schema() -> None:
"""
Test the CONF_SCHEMA is from the right type
"""
assert isinstance(constants.CONF_SCHEMA, dict)

View File

@@ -2,33 +2,31 @@
import pandas import pandas
from freqtrade.analyze import Analyze
from freqtrade.optimize import load_data from freqtrade.optimize import load_data
from freqtrade.strategy.resolver import StrategyResolver from freqtrade.strategy.resolver import StrategyResolver
_pairs = ['ETH/BTC'] _pairs = ['ETH/BTC']
def load_dataframe_pair(pairs): def load_dataframe_pair(pairs, strategy):
ld = load_data(None, ticker_interval='5m', pairs=pairs) ld = load_data(None, ticker_interval='5m', pairs=pairs)
assert isinstance(ld, dict) assert isinstance(ld, dict)
assert isinstance(pairs[0], str) assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]] dataframe = ld[pairs[0]]
analyze = Analyze({'strategy': 'DefaultStrategy'}) dataframe = strategy.analyze_ticker(dataframe, {'pair': pairs[0]})
dataframe = analyze.analyze_ticker(dataframe)
return dataframe return dataframe
def test_dataframe_load(): def test_dataframe_load():
StrategyResolver({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy
dataframe = load_dataframe_pair(_pairs) dataframe = load_dataframe_pair(_pairs, strategy)
assert isinstance(dataframe, pandas.core.frame.DataFrame) assert isinstance(dataframe, pandas.core.frame.DataFrame)
def test_dataframe_columns_exists(): def test_dataframe_columns_exists():
StrategyResolver({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy
dataframe = load_dataframe_pair(_pairs) dataframe = load_dataframe_pair(_pairs, strategy)
assert 'high' in dataframe.columns assert 'high' in dataframe.columns
assert 'low' in dataframe.columns assert 'low' in dataframe.columns
assert 'close' in dataframe.columns assert 'close' in dataframe.columns

View File

@@ -5,7 +5,6 @@ import time
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from requests.exceptions import RequestException from requests.exceptions import RequestException
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
@@ -184,6 +183,24 @@ def test_fiat_convert_without_network(mocker):
CryptoToFiatConverter._coinmarketcap = cmc_temp CryptoToFiatConverter._coinmarketcap = cmc_temp
def test_fiat_invalid_response(mocker, caplog):
# Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}")
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
listings=listmock,
)
# with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {}
fiat_convert._load_cryptomap()
length_cryptomap = len(fiat_convert._cryptomap)
assert length_cryptomap == 0
assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError',
caplog.record_tuples)
def test_convert_amount(mocker): def test_convert_amount(mocker):
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
# pragma pylint: disable=missing-docstring
import pandas as pd import pandas as pd
from freqtrade.indicator_helpers import went_up, went_down from freqtrade.indicator_helpers import went_down, went_up
def test_went_up(): def test_went_up():

View File

@@ -1,8 +1,5 @@
""" # pragma pylint: disable=missing-docstring
Unit test file for main.py
"""
import logging
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
@@ -11,7 +8,7 @@ import pytest
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main, set_loggers, reconfigure from freqtrade.main import main, reconfigure
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
@@ -27,49 +24,24 @@ def test_parse_args_backtesting(mocker) -> None:
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.live is False
assert call_args.loglevel == 20 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
def test_main_start_hyperopt(mocker) -> None: def test_main_start_hyperopt(mocker) -> None:
"""
Test that main() can start hyperopt
"""
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
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 == 20 assert call_args.loglevel == 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
def test_set_loggers() -> None:
"""
Test set_loggers() update the logger level for third-party libraries
"""
previous_value1 = logging.getLogger('requests.packages.urllib3').level
previous_value2 = logging.getLogger('telegram').level
set_loggers()
value1 = logging.getLogger('requests.packages.urllib3').level
assert previous_value1 is not value1
assert value1 is logging.INFO
value2 = logging.getLogger('telegram').level
assert previous_value2 is not value2
assert value2 is logging.INFO
def test_main_fatal_exception(mocker, default_conf, caplog) -> None: def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
"""
Test main() function
In this test we are skipping the while True loop by throwing an exception.
"""
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
@@ -81,7 +53,6 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf lambda *args, **kwargs: default_conf
) )
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example'] args = ['-c', 'config.json.example']
@@ -94,10 +65,6 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
"""
Test main() function
In this test we are skipping the while True loop by throwing an exception.
"""
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
@@ -109,7 +76,6 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf lambda *args, **kwargs: default_conf
) )
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example'] args = ['-c', 'config.json.example']
@@ -122,10 +88,6 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
def test_main_operational_exception(mocker, default_conf, caplog) -> None: def test_main_operational_exception(mocker, default_conf, caplog) -> None:
"""
Test main() function
In this test we are skipping the while True loop by throwing an exception.
"""
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
@@ -137,7 +99,6 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf lambda *args, **kwargs: default_conf
) )
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example'] args = ['-c', 'config.json.example']
@@ -150,10 +111,6 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
def test_main_reload_conf(mocker, default_conf, caplog) -> None: def test_main_reload_conf(mocker, default_conf, caplog) -> None:
"""
Test main() function
In this test we are skipping the while True loop by throwing an exception.
"""
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
@@ -165,7 +122,6 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf lambda *args, **kwargs: default_conf
) )
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
# Raise exception as side effect to avoid endless loop # Raise exception as side effect to avoid endless loop
@@ -181,7 +137,6 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None:
def test_reconfigure(mocker, default_conf) -> None: def test_reconfigure(mocker, default_conf) -> None:
""" Test recreate() function """
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
@@ -193,7 +148,6 @@ def test_reconfigure(mocker, default_conf) -> None:
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf lambda *args, **kwargs: default_conf
) )
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)

View File

@@ -1,34 +1,23 @@
# pragma pylint: disable=missing-docstring,C0103 # pragma pylint: disable=missing-docstring,C0103
"""
Unit test file for misc.py
"""
import datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.analyze import Analyze from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray, from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
common_datearray, file_dump_json, format_ms_time) file_dump_json, format_ms_time, shorten_date)
from freqtrade.optimize.__init__ import load_tickerdata_file from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.strategy.default_strategy import DefaultStrategy
def test_shorten_date() -> None: def test_shorten_date() -> None:
"""
Test shorten_date() function
:return: None
"""
str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago' str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago'
str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago' str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago'
assert shorten_date(str_data) == str_shorten_data assert shorten_date(str_data) == str_shorten_data
def test_datesarray_to_datetimearray(ticker_history): def test_datesarray_to_datetimearray(ticker_history):
""" dataframes = parse_ticker_dataframe(ticker_history)
Test datesarray_to_datetimearray() function
:return: None
"""
dataframes = Analyze.parse_ticker_dataframe(ticker_history)
dates = datesarray_to_datetimearray(dataframes['date']) dates = datesarray_to_datetimearray(dataframes['date'])
assert isinstance(dates[0], datetime.datetime) assert isinstance(dates[0], datetime.datetime)
@@ -43,14 +32,10 @@ def test_datesarray_to_datetimearray(ticker_history):
def test_common_datearray(default_conf) -> None: def test_common_datearray(default_conf) -> None:
""" strategy = DefaultStrategy(default_conf)
Test common_datearray()
:return: None
"""
analyze = Analyze(default_conf)
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick} tickerlist = {'UNITTEST/BTC': tick}
dataframes = analyze.tickerdata_to_dataframe(tickerlist) dataframes = strategy.tickerdata_to_dataframe(tickerlist)
dates = common_datearray(dataframes) dates = common_datearray(dataframes)
@@ -60,10 +45,6 @@ def test_common_datearray(default_conf) -> None:
def test_file_dump_json(mocker) -> None: def test_file_dump_json(mocker) -> None:
"""
Test file_dump_json()
:return: None
"""
file_open = mocker.patch('freqtrade.misc.open', MagicMock()) file_open = mocker.patch('freqtrade.misc.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock()) json_dump = mocker.patch('json.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3]) file_dump_json('somefile', [1, 2, 3])
@@ -77,10 +58,6 @@ def test_file_dump_json(mocker) -> None:
def test_format_ms_time() -> None: def test_format_ms_time() -> None:
"""
test format_ms_time()
:return: None
"""
# Date 2018-04-10 18:02:01 # Date 2018-04-10 18:02:01
date_in_epoch_ms = 1523383321000 date_in_epoch_ms = 1523383321000
date = format_ms_time(date_in_epoch_ms) date = format_ms_time(date_in_epoch_ms)

View File

@@ -1,12 +1,13 @@
# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=missing-docstring, C0103
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
import logging
import pytest import pytest
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade import constants, OperationalException from freqtrade import OperationalException, constants
from freqtrade.persistence import Trade, init, clean_dry_run_db from freqtrade.persistence import Trade, clean_dry_run_db, init
from freqtrade.tests.conftest import log_has
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
@@ -22,46 +23,40 @@ def test_init_create_session(default_conf):
def test_init_custom_db_url(default_conf, mocker): def test_init_custom_db_url(default_conf, mocker):
conf = deepcopy(default_conf)
# Update path to a value other than default, but still in-memory # Update path to a value other than default, but still in-memory
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
init(conf) init(default_conf)
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
def test_init_invalid_db_url(default_conf): def test_init_invalid_db_url(default_conf):
conf = deepcopy(default_conf)
# Update path to a value other than default, but still in-memory # Update path to a value other than default, but still in-memory
conf.update({'db_url': 'unknown:///some.url'}) default_conf.update({'db_url': 'unknown:///some.url'})
with pytest.raises(OperationalException, match=r'.*no valid database URL*'): with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init(conf) init(default_conf)
def test_init_prod_db(default_conf, mocker): def test_init_prod_db(default_conf, mocker):
conf = deepcopy(default_conf) default_conf.update({'dry_run': False})
conf.update({'dry_run': False}) default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
init(conf) init(default_conf)
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
def test_init_dryrun_db(default_conf, mocker): def test_init_dryrun_db(default_conf, mocker):
conf = deepcopy(default_conf) default_conf.update({'dry_run': True})
conf.update({'dry_run': True}) default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
init(conf) init(default_conf)
assert create_engine_mock.call_count == 1 assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://' assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
@@ -400,13 +395,18 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.stake_amount == default_conf.get("stake_amount") assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC" assert trade.pair == "ETC/BTC"
assert trade.exchange == "bittrex" assert trade.exchange == "bittrex"
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
def test_migrate_new(mocker, default_conf, fee): def test_migrate_new(mocker, default_conf, fee, caplog):
""" """
Test Database migration (starting with new pairformat) Test Database migration (starting with new pairformat)
""" """
caplog.set_level(logging.DEBUG)
amount = 103.223 amount = 103.223
# Always create all columns apart from the last!
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" ( create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL, id INTEGER NOT NULL,
exchange VARCHAR NOT NULL, exchange VARCHAR NOT NULL,
@@ -421,14 +421,21 @@ def test_migrate_new(mocker, default_conf, fee):
open_date DATETIME NOT NULL, open_date DATETIME NOT NULL,
close_date DATETIME, close_date DATETIME,
open_order_id VARCHAR, open_order_id VARCHAR,
stop_loss FLOAT,
initial_stop_loss FLOAT,
max_rate FLOAT,
sell_reason VARCHAR,
strategy VARCHAR,
PRIMARY KEY (id), PRIMARY KEY (id),
CHECK (is_open IN (0, 1)) CHECK (is_open IN (0, 1))
);""" );"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee, insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date) open_rate, stake_amount, amount, open_date,
stop_loss, initial_stop_loss, max_rate)
VALUES ('binance', 'ETC/BTC', 1, {fee}, VALUES ('binance', 'ETC/BTC', 1, {fee},
0.00258580, {stake}, {amount}, 0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000') '2019-11-28 12:44:24.000000',
0.0, 0.0, 0.0)
""".format(fee=fee.return_value, """.format(fee=fee.return_value,
stake=default_conf.get("stake_amount"), stake=default_conf.get("stake_amount"),
amount=amount amount=amount
@@ -439,6 +446,11 @@ def test_migrate_new(mocker, default_conf, fee):
# Create table using the old format # Create table using the old format
engine.execute(create_table_old) engine.execute(create_table_old)
engine.execute(insert_table_old) engine.execute(insert_table_old)
# fake previous backup
engine.execute("create table trades_bak as select * from trades")
engine.execute("create table trades_bak1 as select * from trades")
# Run init to test migration # Run init to test migration
init(default_conf) init(default_conf)
@@ -453,3 +465,121 @@ def test_migrate_new(mocker, default_conf, fee):
assert trade.stake_amount == default_conf.get("stake_amount") assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC" assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance" assert trade.exchange == "binance"
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert trade.sell_reason is None
assert trade.strategy is None
assert trade.ticker_interval is None
assert log_has("trying trades_bak1", caplog.record_tuples)
assert log_has("trying trades_bak2", caplog.record_tuples)
assert log_has("Running database migration - backup available as trades_bak2",
caplog.record_tuples)
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
"""
Test Database migration (starting with new pairformat)
"""
caplog.set_level(logging.DEBUG)
amount = 103.223
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee_open FLOAT NOT NULL,
fee_close FLOAT NOT NULL,
open_rate FLOAT,
close_rate FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close,
open_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee},
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
engine.execute(create_table_old)
engine.execute(insert_table_old)
# Run init to test migration
init(default_conf)
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
assert trade.fee_open == fee.return_value
assert trade.fee_close == fee.return_value
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance"
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert log_has("trying trades_bak0", caplog.record_tuples)
assert log_has("Running database migration - backup available as trades_bak0",
caplog.record_tuples)
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
)
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
assert trade.stop_loss == 0.95
assert trade.max_rate == 1
assert trade.initial_stop_loss == 0.95
# Get percent of profit with a lowre rate
trade.adjust_stop_loss(0.96, 0.05)
assert trade.stop_loss == 0.95
assert trade.max_rate == 1
assert trade.initial_stop_loss == 0.95
# Get percent of profit with a custom rate (Higher than open rate)
trade.adjust_stop_loss(1.3, -0.1)
assert round(trade.stop_loss, 8) == 1.17
assert trade.max_rate == 1.3
assert trade.initial_stop_loss == 0.95
# current rate lower again ... should not change
trade.adjust_stop_loss(1.2, 0.1)
assert round(trade.stop_loss, 8) == 1.17
assert trade.max_rate == 1.3
assert trade.initial_stop_loss == 0.95
# current rate higher... should raise stoploss
trade.adjust_stop_loss(1.4, 0.1)
assert round(trade.stop_loss, 8) == 1.26
assert trade.max_rate == 1.4
assert trade.initial_stop_loss == 0.95
# Initial is true but stop_loss set - so doesn't do anything
trade.adjust_stop_loss(1.7, 0.1, True)
assert round(trade.stop_loss, 8) == 1.26
assert trade.max_rate == 1.4
assert trade.initial_stop_loss == 0.95

View File

@@ -1,14 +0,0 @@
"""
Unit test file for constants.py
"""
from freqtrade.state import State
def test_state_object() -> None:
"""
Test the State object has the mandatory states
:return: None
"""
assert hasattr(State, 'RUNNING')
assert hasattr(State, 'STOPPED')

View File

@@ -0,0 +1,16 @@
import talib.abstract as ta
import pandas as pd
def test_talib_bollingerbands_near_zero_values():
inputs = pd.DataFrame([
{'close': 0.00000010},
{'close': 0.00000011},
{'close': 0.00000012},
{'close': 0.00000013},
{'close': 0.00000014}
])
bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2)
assert (bollinger['upperband'][3] != bollinger['middleband'][3])

View File

@@ -1,6 +1,6 @@
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
tar zxvf ta-lib-0.4.0-src.tar.gz tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib && ./configure && make && sudo make install && cd .. cd ta-lib && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && ./configure && make && sudo make install && cd ..
else else
echo "TA-lib already installed, skipping download and build." echo "TA-lib already installed, skipping download and build."
cd ta-lib && sudo make install && cd .. cd ta-lib && sudo make install && cd ..

View File

@@ -1,25 +1,26 @@
ccxt==1.14.256 ccxt==1.17.363
SQLAlchemy==1.2.8 SQLAlchemy==1.2.12
python-telegram-bot==10.1.0 python-telegram-bot==11.1.0
arrow==0.12.1 arrow==0.12.1
cachetools==2.1.0 cachetools==2.1.0
requests==2.19.1 requests==2.19.1
urllib3==1.22 urllib3==1.22
wrapt==1.10.11 wrapt==1.10.11
pandas==0.23.1 pandas==0.23.4
scikit-learn==0.19.1 scikit-learn==0.20.0
scipy==1.1.0 scipy==1.1.0
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.14.5 numpy==1.15.2
TA-Lib==0.4.17 TA-Lib==0.4.17
pytest==3.6.2 pytest==3.8.1
pytest-mock==1.10.0 pytest-mock==1.10.0
pytest-cov==2.5.1 pytest-asyncio==0.9.0
hyperopt==0.1 pytest-cov==2.6.0
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
networkx==1.11 # pyup: ignore
tabulate==0.8.2 tabulate==0.8.2
coinmarketcap==5.0.3 coinmarketcap==5.0.3
# Required for hyperopt
scikit-optimize==0.5.2
# Required for plotting data # Required for plotting data
#plotly==2.7.0 #plotly==3.1.1

View File

@@ -143,15 +143,14 @@ def convert_main(args: Namespace) -> None:
interval = str_interval interval = str_interval
break break
# change order on pairs if old ticker interval found # change order on pairs if old ticker interval found
filename_new = path.join(path.dirname(filename), filename_new = path.join(path.dirname(filename),
"{}_{}-{}.json".format(currencies[1], f"{currencies[1]}_{currencies[0]}-{interval}.json")
currencies[0], interval))
elif ret_string: elif ret_string:
interval = ret_string.group(0) interval = ret_string.group(0)
filename_new = path.join(path.dirname(filename), filename_new = path.join(path.dirname(filename),
"{}_{}-{}.json".format(currencies[0], f"{currencies[0]}_{currencies[1]}-{interval}.json")
currencies[1], interval))
else: else:
logger.warning("file %s could not be converted, interval not found", filename) logger.warning("file %s could not be converted, interval not found", filename)

View File

@@ -1,13 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""This script generate json data from bittrex""" """This script generate json data"""
import json import json
import sys import sys
import os from pathlib import Path
import arrow import arrow
from freqtrade import (arguments, misc) from freqtrade import arguments
from freqtrade.arguments import TimeRange
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.optimize import download_backtesting_testdata
DEFAULT_DL_PATH = 'user_data/data' DEFAULT_DL_PATH = 'user_data/data'
@@ -17,25 +20,27 @@ args = arguments.parse_args()
timeframes = args.timeframes timeframes = args.timeframes
dl_path = os.path.join(DEFAULT_DL_PATH, args.exchange) dl_path = Path(DEFAULT_DL_PATH).joinpath(args.exchange)
if args.export: if args.export:
dl_path = args.export dl_path = Path(args.export)
if not os.path.isdir(dl_path): if not dl_path.is_dir():
sys.exit(f'Directory {dl_path} does not exist.') sys.exit(f'Directory {dl_path} does not exist.')
pairs_file = args.pairs_file if args.pairs_file else os.path.join(dl_path, 'pairs.json') pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json')
if not os.path.isfile(pairs_file): if not pairs_file.exists():
sys.exit(f'No pairs file found with path {pairs_file}.') sys.exit(f'No pairs file found with path {pairs_file}.')
with open(pairs_file) as file: with pairs_file.open() as file:
PAIRS = list(set(json.load(file))) PAIRS = list(set(json.load(file)))
PAIRS.sort() PAIRS.sort()
since_time = None
timerange = TimeRange()
if args.days: if args.days:
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000 time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d")
timerange = arguments.parse_timerange(f'{time_since}-')
print(f'About to download pairs: {PAIRS} to {dl_path}') print(f'About to download pairs: {PAIRS} to {dl_path}')
@@ -47,9 +52,10 @@ exchange = Exchange({'key': '',
'stake_currency': '', 'stake_currency': '',
'dry_run': True, 'dry_run': True,
'exchange': { 'exchange': {
'name': args.exchange, 'name': args.exchange,
'pair_whitelist': [] 'pair_whitelist': [],
} 'ccxt_rate_limit': False
}
}) })
pairs_not_available = [] pairs_not_available = []
@@ -59,21 +65,18 @@ for pair in PAIRS:
print(f"skipping pair {pair}") print(f"skipping pair {pair}")
continue continue
for tick_interval in timeframes: for tick_interval in timeframes:
print(f'downloading pair {pair}, interval {tick_interval}')
data = exchange.get_ticker_history(pair, tick_interval, since_ms=since_time)
if not data:
print('\tNo data was downloaded')
break
print('\tData was downloaded for period %s - %s' % (
arrow.get(data[0][0] / 1000).format(),
arrow.get(data[-1][0] / 1000).format()))
# save data
pair_print = pair.replace('/', '_') pair_print = pair.replace('/', '_')
filename = f'{pair_print}-{tick_interval}.json' filename = f'{pair_print}-{tick_interval}.json'
misc.file_dump_json(os.path.join(dl_path, filename), data) dl_file = dl_path.joinpath(filename)
if args.erase and dl_file.exists():
print(f'Deleting existing data for pair {pair}, interval {tick_interval}')
dl_file.unlink()
print(f'downloading pair {pair}, interval {tick_interval}')
download_backtesting_testdata(str(dl_path), exchange=exchange,
pair=pair,
tick_interval=tick_interval,
timerange=timerange)
if pairs_not_available: if pairs_not_available:

View File

@@ -0,0 +1,93 @@
import os
import sys
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(root + '/python')
import ccxt # noqa: E402
def style(s, style):
return style + s + '\033[0m'
def green(s):
return style(s, '\033[92m')
def blue(s):
return style(s, '\033[94m')
def yellow(s):
return style(s, '\033[93m')
def red(s):
return style(s, '\033[91m')
def pink(s):
return style(s, '\033[95m')
def bold(s):
return style(s, '\033[1m')
def underline(s):
return style(s, '\033[4m')
def dump(*args):
print(' '.join([str(arg) for arg in args]))
def print_supported_exchanges():
dump('Supported exchanges:', green(', '.join(ccxt.exchanges)))
try:
id = sys.argv[1] # get exchange id from command line arguments
# check if the exchange is supported by ccxt
exchange_found = id in ccxt.exchanges
if exchange_found:
dump('Instantiating', green(id), 'exchange')
# instantiate the exchange by id
exchange = getattr(ccxt, id)({
# 'proxy':'https://cors-anywhere.herokuapp.com/',
})
# load all markets from the exchange
markets = exchange.load_markets()
# output a list of all market symbols
dump(green(id), 'has', len(exchange.symbols), 'symbols:', exchange.symbols)
tuples = list(ccxt.Exchange.keysort(markets).items())
# debug
for (k, v) in tuples:
print(v)
# output a table of all markets
dump(pink('{:<15} {:<15} {:<15} {:<15}'.format('id', 'symbol', 'base', 'quote')))
for (k, v) in tuples:
dump('{:<15} {:<15} {:<15} {:<15}'.format(v['id'], v['symbol'], v['base'], v['quote']))
else:
dump('Exchange ' + red(id) + ' not found')
print_supported_exchanges()
except Exception as e:
dump('[' + type(e).__name__ + ']', str(e))
dump("Usage: python " + sys.argv[0], green('id'))
print_supported_exchanges()

View File

@@ -24,27 +24,76 @@ Example of usage:
> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3 > python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3
--indicators2 fastk,fastd --indicators2 fastk,fastd
""" """
import json
import logging import logging
import os
import sys import sys
from argparse import Namespace from argparse import Namespace
from pathlib import Path
from typing import Dict, List, Any from typing import Dict, List, Any
import pandas as pd
import plotly.graph_objs as go import plotly.graph_objs as go
import pytz
from plotly import tools from plotly import tools
from plotly.offline import plot from plotly.offline import plot
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import persistence from freqtrade import persistence
from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments, TimeRange
from freqtrade.arguments import Arguments
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.optimize.backtesting import setup_configuration from freqtrade.optimize.backtesting import setup_configuration
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import StrategyResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_CONF: Dict[str, Any] = {} _CONF: Dict[str, Any] = {}
timeZone = pytz.UTC
def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame:
trades: pd.DataFrame = pd.DataFrame()
if args.db_url:
persistence.init(_CONF)
columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"]
for x in Trade.query.all():
print("date: {}".format(x.open_date))
trades = pd.DataFrame([(t.pair, t.calc_profit(),
t.open_date.replace(tzinfo=timeZone),
t.close_date.replace(tzinfo=timeZone) if t.close_date else None,
t.open_rate, t.close_rate,
t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None)
for t in Trade.query.filter(Trade.pair.is_(pair)).all()],
columns=columns)
elif args.exportfilename:
file = Path(args.exportfilename)
# must align with columns in backtest.py
columns = ["pair", "profit", "opents", "closets", "index", "duration",
"open_rate", "close_rate", "open_at_end", "sell_reason"]
with file.open() as f:
data = json.load(f)
trades = pd.DataFrame(data, columns=columns)
trades = trades.loc[trades["pair"] == pair]
if timerange:
if timerange.starttype == 'date':
trades = trades.loc[trades["opents"] >= timerange.startts]
if timerange.stoptype == 'date':
trades = trades.loc[trades["opents"] <= timerange.stopts]
trades['opents'] = pd.to_datetime(trades['opents'],
unit='s',
utc=True,
infer_datetime_format=True)
trades['closets'] = pd.to_datetime(trades['closets'],
unit='s',
utc=True,
infer_datetime_format=True)
return trades
def plot_analyzed_dataframe(args: Namespace) -> None: def plot_analyzed_dataframe(args: Namespace) -> None:
""" """
@@ -56,6 +105,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
# Load the configuration # Load the configuration
_CONF.update(setup_configuration(args)) _CONF.update(setup_configuration(args))
print(_CONF)
# Set the pair to audit # Set the pair to audit
pair = args.pair pair = args.pair
@@ -72,7 +122,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
# Load the strategy # Load the strategy
try: try:
analyze = Analyze(_CONF) strategy = StrategyResolver(_CONF).strategy
exchange = Exchange(_CONF) exchange = Exchange(_CONF)
except AttributeError: except AttributeError:
logger.critical( logger.critical(
@@ -82,20 +132,22 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
exit() exit()
# Set the ticker to use # Set the ticker to use
tick_interval = analyze.get_ticker_interval() tick_interval = strategy.ticker_interval
# Load pair tickers # Load pair tickers
tickers = {} tickers = {}
if args.live: if args.live:
logger.info('Downloading pair.') logger.info('Downloading pair.')
tickers[pair] = exchange.get_ticker_history(pair, tick_interval) exchange.refresh_tickers([pair], tick_interval)
tickers[pair] = exchange.klines[pair]
else: else:
tickers = optimize.load_data( tickers = optimize.load_data(
datadir=_CONF.get("datadir"), datadir=_CONF.get("datadir"),
pairs=[pair], pairs=[pair],
ticker_interval=tick_interval, ticker_interval=tick_interval,
refresh_pairs=_CONF.get('refresh_pairs', False), refresh_pairs=_CONF.get('refresh_pairs', False),
timerange=timerange timerange=timerange,
exchange=Exchange(_CONF)
) )
# No ticker found, or impossible to download # No ticker found, or impossible to download
@@ -103,30 +155,31 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
exit() exit()
# Get trades already made from the DB # Get trades already made from the DB
trades: List[Trade] = [] trades = load_trades(args, pair, timerange)
if args.db_url:
persistence.init(_CONF) dataframes = strategy.tickerdata_to_dataframe(tickers)
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
dataframes = analyze.tickerdata_to_dataframe(tickers)
dataframe = dataframes[pair] dataframe = dataframes[pair]
dataframe = analyze.populate_buy_trend(dataframe) dataframe = strategy.advise_buy(dataframe, {'pair': pair})
dataframe = analyze.populate_sell_trend(dataframe) dataframe = strategy.advise_sell(dataframe, {'pair': pair})
if len(dataframe.index) > 750: if len(dataframe.index) > args.plot_limit:
logger.warning('Ticker contained more than 750 candles, clipping.') logger.warning('Ticker contained more than %s candles as defined '
'with --plot-limit, clipping.', args.plot_limit)
dataframe = dataframe.tail(args.plot_limit)
trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']]
fig = generate_graph( fig = generate_graph(
pair=pair, pair=pair,
trades=trades, trades=trades,
data=dataframe.tail(750), data=dataframe,
args=args args=args
) )
plot(fig, filename=os.path.join('user_data', 'freqtrade-plot.html')) plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')))
def generate_graph(pair, trades, data, args) -> tools.make_subplots: def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tools.make_subplots:
""" """
Generate the graph from the data generated by Backtesting or from DB Generate the graph from the data generated by Backtesting or from DB
:param pair: Pair to Display on the graph :param pair: Pair to Display on the graph
@@ -187,8 +240,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
) )
trade_buys = go.Scattergl( trade_buys = go.Scattergl(
x=[t.open_date.isoformat() for t in trades], x=trades["opents"],
y=[t.open_rate for t in trades], y=trades["open_rate"],
mode='markers', mode='markers',
name='trade_buy', name='trade_buy',
marker=dict( marker=dict(
@@ -199,8 +252,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
) )
) )
trade_sells = go.Scattergl( trade_sells = go.Scattergl(
x=[t.close_date.isoformat() for t in trades], x=trades["closets"],
y=[t.close_rate for t in trades], y=trades["close_rate"],
mode='markers', mode='markers',
name='trade_sell', name='trade_sell',
marker=dict( marker=dict(
@@ -219,7 +272,7 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
x=data.date, x=data.date,
y=data.bb_lowerband, y=data.bb_lowerband,
name='BB lower', name='BB lower',
line={'color': "transparent"}, line={'color': 'rgba(255,255,255,0)'},
) )
bb_upper = go.Scatter( bb_upper = go.Scatter(
x=data.date, x=data.date,
@@ -227,7 +280,7 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
name='BB upper', name='BB upper',
fill="tonexty", fill="tonexty",
fillcolor="rgba(0,176,246,0.2)", fillcolor="rgba(0,176,246,0.2)",
line={'color': "transparent"}, line={'color': 'rgba(255,255,255,0)'},
) )
fig.append_trace(bb_lower, 1, 1) fig.append_trace(bb_lower, 1, 1)
fig.append_trace(bb_upper, 1, 1) fig.append_trace(bb_upper, 1, 1)
@@ -299,11 +352,17 @@ def plot_parse_args(args: List[str]) -> Namespace:
default='macd', default='macd',
dest='indicators2', dest='indicators2',
) )
arguments.parser.add_argument(
'--plot-limit',
help='Specify tick limit for plotting - too high values cause huge files - '
'Default: %(default)s',
dest='plot_limit',
default=750,
type=int,
)
arguments.common_args_parser() arguments.common_args_parser()
arguments.optimizer_shared_options(arguments.parser) arguments.optimizer_shared_options(arguments.parser)
arguments.backtesting_options(arguments.parser) arguments.backtesting_options(arguments.parser)
return arguments.parse_args() return arguments.parse_args()

View File

@@ -26,9 +26,8 @@ import plotly.graph_objs as go
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.analyze import Analyze
from freqtrade import constants from freqtrade import constants
from freqtrade.strategy.resolver import StrategyResolver
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
import freqtrade.misc as misc import freqtrade.misc as misc
@@ -87,7 +86,8 @@ def plot_profit(args: Namespace) -> None:
# Init strategy # Init strategy
try: try:
analyze = Analyze({'strategy': config.get('strategy')}) strategy = StrategyResolver({'strategy': config.get('strategy')}).strategy
except AttributeError: except AttributeError:
logger.critical( logger.critical(
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"', 'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
@@ -113,7 +113,7 @@ def plot_profit(args: Namespace) -> None:
else: else:
filter_pairs = config['exchange']['pair_whitelist'] filter_pairs = config['exchange']['pair_whitelist']
tick_interval = analyze.strategy.ticker_interval tick_interval = strategy.ticker_interval
pairs = config['exchange']['pair_whitelist'] pairs = config['exchange']['pair_whitelist']
if filter_pairs: if filter_pairs:
@@ -127,7 +127,7 @@ def plot_profit(args: Namespace) -> None:
refresh_pairs=False, refresh_pairs=False,
timerange=timerange timerange=timerange
) )
dataframes = analyze.tickerdata_to_dataframe(tickers) dataframes = strategy.tickerdata_to_dataframe(tickers)
# NOTE: the dataframes are of unequal length, # NOTE: the dataframes are of unequal length,
# 'dates' is an merged date array of them all. # 'dates' is an merged date array of them all.

View File

@@ -18,7 +18,7 @@ setup(name='freqtrade',
license='GPLv3', license='GPLv3',
packages=['freqtrade'], packages=['freqtrade'],
scripts=['bin/freqtrade'], scripts=['bin/freqtrade'],
setup_requires=['pytest-runner'], setup_requires=['pytest-runner', 'numpy'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[ install_requires=[
'ccxt', 'ccxt',
@@ -36,6 +36,7 @@ setup(name='freqtrade',
'tabulate', 'tabulate',
'cachetools', 'cachetools',
'coinmarketcap', 'coinmarketcap',
'scikit-optimize',
], ],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,

View File

@@ -1,13 +1,31 @@
#!/usr/bin/env bash #!/usr/bin/env bash
#encoding=utf8 #encoding=utf8
# Check which python version is installed
function check_installed_python() {
which python3.7
if [ $? -eq 0 ]; then
echo "using Python 3.7"
PYTHON=python3.7
return
fi
which python3.6
if [ $? -eq 0 ]; then
echo "using Python 3.6"
PYTHON=python3.6
return
fi
}
function updateenv () { function updateenv () {
echo "-------------------------" echo "-------------------------"
echo "Update your virtual env" echo "Update your virtual env"
echo "-------------------------" echo "-------------------------"
source .env/bin/activate source .env/bin/activate
echo "pip3 install in-progress. Please wait..." echo "pip3 install in-progress. Please wait..."
pip3.6 install --quiet --upgrade pip pip3 install --quiet --upgrade pip
pip3 install --quiet -r requirements.txt --upgrade pip3 install --quiet -r requirements.txt --upgrade
pip3 install --quiet -r requirements.txt pip3 install --quiet -r requirements.txt
pip3 install --quiet -e . pip3 install --quiet -e .
@@ -79,7 +97,7 @@ function reset () {
fi fi
echo echo
python3.6 -m venv .env ${PYTHON} -m venv .env
updateenv updateenv
} }
@@ -183,7 +201,7 @@ function install () {
install_debian install_debian
else else
echo "This script does not support your OS." echo "This script does not support your OS."
echo "If you have Python3.6, pip, virtualenv, ta-lib you can continue." echo "If you have Python3.6 or Python3.7, pip, virtualenv, ta-lib you can continue."
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell." echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
sleep 10 sleep 10
fi fi
@@ -193,7 +211,7 @@ function install () {
echo "-------------------------" echo "-------------------------"
echo "Run the bot" echo "Run the bot"
echo "-------------------------" echo "-------------------------"
echo "You can now use the bot by executing 'source .env/bin/activate; python3.6 freqtrade/main.py'." echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'."
} }
function plot () { function plot () {
@@ -214,6 +232,9 @@ function help () {
echo " -p,--plot Install dependencies for Plotting scripts." echo " -p,--plot Install dependencies for Plotting scripts."
} }
# Verify if 3.6 or 3.7 is installed
check_installed_python
case $* in case $* in
--install|-i) --install|-i)
install install

View File

@@ -12,11 +12,13 @@ import numpy # noqa
# This class is a sample. Feel free to customize it. # This class is a sample. Feel free to customize it.
class TestStrategy(IStrategy): class TestStrategy(IStrategy):
__test__ = False # pytest expects to find tests here because of the name
""" """
This is a test strategy to inspire you. This is a test strategy to inspire you.
More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
You can: You can:
:return: a Dataframe with all mandatory indicators for the strategies
- Rename the class name (Do not forget to update class_name) - Rename the class name (Do not forget to update class_name)
- Add any methods you want to build your strategy - Add any methods you want to build your strategy
- Add any lib you need to build your strategy - Add any lib you need to build your strategy
@@ -43,13 +45,19 @@ class TestStrategy(IStrategy):
# Optimal ticker interval for the strategy # Optimal ticker interval for the strategy
ticker_interval = '5m' ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: # run "populate_indicators" only for new candle
ta_on_candle = False
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage. or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe()
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
""" """
# Momentum Indicator # Momentum Indicator
@@ -210,10 +218,11 @@ class TestStrategy(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
@@ -226,10 +235,11 @@ class TestStrategy(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[