Compare commits

...

653 Commits

Author SHA1 Message Date
Samuel Husso
2bb63ba33d Merge pull request #953 from freqtrade/release-0.17.0
Release 0.17.0
2018-06-23 16:22:51 -05:00
Samuel Husso
46a062d5fb Drafting freqtrade 0.17.0 release 2018-06-23 09:35:52 -05:00
Samuel Husso
8b7183cdbc Merge pull request #951 from freqtrade/readme-update
README: note to open an issue before starting major feature work
2018-06-23 09:32:56 -05:00
Michael Egger
beb15532f7 Merge pull request #950 from freqtrade/fix-filenotfounderror
StrategyResolver: Don't fail if user_data isn't present
2018-06-23 16:07:52 +02:00
Michael Egger
107f3ed35b Merge pull request #760 from arudov/feature-unlimited-stake_amount
Feature unlimited stake amount
2018-06-23 16:07:38 +02:00
Anton
f82b809fcf Merge with develop 2018-06-23 16:50:27 +03:00
Samuel Husso
9bad75f37d README: note to open an issue before starting major feature work 2018-06-23 08:36:32 -05:00
Samuel Husso
864bbc441a Merge pull request #882 from freqtrade/feature/revamp_readme
Update the README structure
2018-06-23 08:21:56 -05:00
Michael Egger
e2df908304 Merge pull request #949 from freqtrade/pyup-scheduled-update-2018-06-23
Scheduled daily dependency update on saturday
2018-06-23 14:56:52 +02:00
gcarq
4ea5fcc661 resolver: don't fail if user_data can't be found 2018-06-23 14:42:22 +02:00
gcarq
9c66c25890 resolver: use current folder instead of script folder to find user_data 2018-06-23 14:34:36 +02:00
pyup-bot
925b9b0c19 Update ccxt from 1.14.253 to 1.14.256 2018-06-23 14:23:07 +02:00
Matthias
e25d8f9435 Merge pull request #947 from freqtrade/code-cleanup
Remove global config from persistence module
2018-06-23 14:21:42 +02:00
gcarq
0b3e4f6bcd remove dead code 2018-06-23 13:50:49 +02:00
gcarq
295dfe2652 persistence: remove obsolete global _CONF variable 2018-06-23 13:50:22 +02:00
Michael Egger
df9015a7f1 Merge pull request #942 from xmatthias/feat/buy_on_sell_first
Introduce ignore_roi_if_buy_signal parameter to avoid sell/buy scenarios
2018-06-23 13:42:03 +02:00
Janne Sinivirta
90caa09ae0 Merge pull request #944 from freqtrade/improve-strategy-handling
Improve strategy handling
2018-06-23 14:32:39 +03:00
Michael Egger
909fd39b80 Merge pull request #945 from freqtrade/update_plotly
update plotly
2018-06-23 13:15:15 +02:00
xmatthias
d23cd73ba8 update plotly 2018-06-23 13:12:36 +02:00
xmatthias
fc219b4e94 move experimental eval below stop_loss_reached to improve performance 2018-06-23 13:10:08 +02:00
gcarq
818a6b12ed tests: add dir() assertion 2018-06-23 11:57:26 +02:00
gcarq
4bd61df3a7 implement test for import_strategy 2018-06-23 11:14:31 +02:00
gcarq
c40e6a12d1 move logic from hyperopt to freqtrade.strategy 2018-06-23 11:13:49 +02:00
gcarq
3360bf4001 wrap strategies with HyperoptStrategy for module lookups with pickle 2018-06-23 10:42:33 +02:00
Michael Egger
168ed91fe1 Merge pull request #941 from freqtrade/avoid-fee-calls-backtesting
avoid calling exchange.get_fee inside loop
2018-06-23 08:17:25 +02:00
Janne Sinivirta
9a07d57ed7 fix flake8 2018-06-23 07:58:25 +03:00
xmatthias
2be7b3d9eb fix mocked bid-value to match limt_buy_order config 2018-06-22 21:24:21 +02:00
xmatthias
e2a2a0be9b extract stop_loss_reached to allow check before ignore_roi_if_buy_signal 2018-06-22 21:21:34 +02:00
Janne Sinivirta
f7e5d2c3a5 check that we set fee on backtesting init 2018-06-22 21:55:09 +03:00
xmatthias
cbfee51f32 introduce experimental variable and fix test naming 2018-06-22 20:51:21 +02:00
xmatthias
8a44dff595 don't sell if buy is still active 2018-06-22 20:23:23 +02:00
Janne Sinivirta
c73b9f5c77 avoid calling exchange.get_fee inside loop 2018-06-22 21:04:07 +03:00
Samuel Husso
c413e94f83 Merge pull request #940 from freqtrade/pyup-scheduled-update-2018-06-22
Scheduled daily dependency update on friday
2018-06-22 16:14:20 +03:00
pyup-bot
98cd8970f9 Update ccxt from 1.14.242 to 1.14.253 2018-06-22 14:24:06 +02:00
Janne Sinivirta
5fcdd3831c Merge pull request #928 from freqtrade/feat/objectify_exchange
Objectify exchange
2018-06-22 06:36:14 +03:00
xmatthias
7f927b4d7a Squashed commit of the following:
commit 435f299bcf
Author: Gert Wohlgemuth <berlinguyinca@gmail.com>
Date:   Wed Jun 20 01:57:28 2018 -0700

    improve readability of outdated history code
2018-06-21 20:47:53 +02:00
Matthias
99e3c6e526 Merge pull request #936 from freqtrade/pyup-scheduled-update-2018-06-21
Scheduled daily dependency update on thursday
2018-06-21 15:20:22 +02:00
pyup-bot
c7976f51e2 Update ccxt from 1.14.230 to 1.14.242 2018-06-21 14:24:06 +02:00
Michael Egger
2c43590268 Merge pull request #933 from freqtrade/pyup-scheduled-update-2018-06-20
Scheduled daily dependency update on wednesday
2018-06-20 14:36:44 +02:00
pyup-bot
36cfea3d0f Update pytest from 3.6.1 to 3.6.2 2018-06-20 14:23:08 +02:00
pyup-bot
a493a2ceef Update ccxt from 1.14.224 to 1.14.230 2018-06-20 14:23:06 +02:00
Michael Egger
96b7273b8f Merge pull request #931 from freqtrade/pyup-scheduled-update-2018-06-19
Scheduled daily dependency update on tuesday
2018-06-19 16:27:30 +02:00
pyup-bot
e66b861c9e Update ccxt from 1.14.211 to 1.14.224 2018-06-19 14:23:05 +02:00
Michael Egger
e0db31e9db Merge pull request #929 from freqtrade/backtest_docker
Update Documentation to include backtesting with docker
2018-06-18 22:54:18 +02:00
xmatthias
a7be15d72f Update Documentation to include backtesting with docker 2018-06-18 22:42:14 +02:00
xmatthias
f7b46d5404 update docstring 2018-06-18 22:34:28 +02:00
xmatthias
488f1717a1 update plot_dataframe script to objectify exchange 2018-06-18 22:32:29 +02:00
xmatthias
2b0ef54595 update download_script for exchange objectify 2018-06-18 22:28:51 +02:00
xmatthias
896afe7118 convert get_name and get_id to properties 2018-06-18 22:20:50 +02:00
xmatthias
ef53134499 lowercase variables 2018-06-18 22:09:46 +02:00
xmatthias
c31519fdb2 lowercase _api object 2018-06-18 22:07:15 +02:00
xmatthias
162f948729 add test for non-configured exchange 2018-06-18 19:56:23 +02:00
xmatthias
ae4c4e77bf standardize exception tests - add one more 2018-06-18 19:46:42 +02:00
xmatthias
695beecf14 add test for get_markets 2018-06-18 19:36:36 +02:00
Samuel Husso
cb015dec7b Merge pull request #927 from freqtrade/pyup-scheduled-update-2018-06-18
Scheduled daily dependency update on monday
2018-06-18 15:47:43 +03:00
pyup-bot
9bc8331667 Update ccxt from 1.14.202 to 1.14.211 2018-06-18 14:23:05 +02:00
xmatthias
520c7feeab Add test for fetch_tickers 2018-06-17 23:38:07 +02:00
xmatthias
1e3d722bc2 add test for get_trades 2018-06-17 23:38:07 +02:00
xmatthias
c9f8dfc6c5 increase get_fee coverage 2018-06-17 23:38:07 +02:00
xmatthias
d156de39f1 Increase test-coverage 2018-06-17 23:38:07 +02:00
xmatthias
2b099a89e4 fix styling issues 2018-06-17 23:38:07 +02:00
xmatthias
6e6ec969eb cleanup mockings 2018-06-17 23:38:07 +02:00
xmatthias
e194af8d25 Streamline validate_pair patching 2018-06-17 23:38:07 +02:00
xmatthias
ace5198475 fix optimize tests 2018-06-17 23:38:07 +02:00
xmatthias
52d36c33cf fix optimie test 2018-06-17 23:38:07 +02:00
xmatthias
251f7db3ca require exchange object to delete pairs 2018-06-17 23:38:07 +02:00
xmatthias
c83e8b7cb5 fix rpc_test 2018-06-17 23:38:07 +02:00
xmatthias
64e09f74a1 fix rpc tests 2018-06-17 23:38:07 +02:00
xmatthias
63b568989a Fix rpc for exchange objectify 2018-06-17 23:38:07 +02:00
xmatthias
975b42caa3 fix tests for exchange objectify 2018-06-17 23:38:07 +02:00
xmatthias
75d02df60d add exchange to call get_singal 2018-06-17 23:38:07 +02:00
xmatthias
082b6077e9 Fix tests analyze 2018-06-17 23:38:07 +02:00
xmatthias
e8ab76f55b fix small in tests 2018-06-17 23:38:07 +02:00
xmatthias
495f15f13c fix exchange tests 2018-06-17 23:38:07 +02:00
xmatthias
68f6423d39 fix most tests 2018-06-17 23:38:07 +02:00
xmatthias
67d345bc08 fix tests for objectify exchange 2018-06-17 23:38:07 +02:00
xmatthias
a159db6863 get_exchange 2018-06-17 23:38:07 +02:00
xmatthias
dea26fadfe move init_ccxt to class 2018-06-17 23:38:07 +02:00
xmatthias
21edcbdc27 Refactor exchange to class 2018-06-17 23:38:07 +02:00
Janne Sinivirta
e3c91df081 Merge pull request #926 from freqtrade/pyup-scheduled-update-2018-06-17
Scheduled daily dependency update on sunday
2018-06-17 16:08:54 +03:00
Janne Sinivirta
c608f1e21e Merge pull request #923 from freqtrade/fix_test_hyperopt
fix hyperopt test when no config.json exists
2018-06-17 16:07:57 +03:00
pyup-bot
fef267a0dc Update ccxt from 1.14.201 to 1.14.202 2018-06-17 14:23:05 +02:00
Michael Egger
5ce2071279 Merge pull request #925 from freqtrade/increase_test_cov_configuration
increase test-coverate for configuration
2018-06-17 13:19:16 +02:00
xmatthias
ad0549414b Revert "also unit tests now need config.json"
This reverts commit 7e2e7946c5.
2018-06-17 11:34:12 +02:00
Janne Sinivirta
c6cc9ae29d Merge pull request #922 from freqtrade/fix_fiat_test
Fix fiat_convert missing mockups
2018-06-17 08:52:03 +03:00
Anton
ae94ab17f4 Merge branch 'develop' into feature-unlimited-stake_amount 2018-06-17 02:23:40 +03:00
Anton
eb909068c5 Add minimal pair stake amount check 2018-06-17 02:23:12 +03:00
xmatthias
90a7fb603d fix typo in coverage-omit 2018-06-16 21:28:41 +02:00
xmatthias
7cfd99d17f exclude __main__.py from coveralls -
if __name__ == '__main__' is close to untestable - and should do nothing
other than calling another function.
2018-06-16 21:00:45 +02:00
xmatthias
972736f0ab increase test-coverate for configureation 2018-06-16 20:55:35 +02:00
Matthias
934974a547 Merge pull request #924 from freqtrade/pyup-scheduled-update-2018-06-16
Scheduled daily dependency update on saturday
2018-06-16 16:14:34 +02:00
pyup-bot
17801871b1 Update ccxt from 1.14.198 to 1.14.201 2018-06-16 14:23:06 +02:00
xmatthias
7564f7e526 fix hyperopt test when no config.json exists 2018-06-16 13:49:03 +02:00
xmatthias
fa00157d12 Fix fiat_convert missing mockups 2018-06-16 13:42:25 +02:00
Matthias
a5511e2e30 Merge pull request #894 from freqtrade/feature/force_close_backtest
Display open trades after backtest period
2018-06-16 12:49:08 +02:00
Janne Sinivirta
0347ce21fd Merge pull request #920 from freqtrade/hyperopt-strip
Remove mongodb from Hyperopt
2018-06-16 10:33:44 +03:00
Janne Sinivirta
7e2e7946c5 also unit tests now need config.json 2018-06-16 09:09:28 +03:00
Janne Sinivirta
0c85febe76 remove all mongodb related code 2018-06-16 09:09:28 +03:00
Janne Sinivirta
c1f8f641e6 remove use of hyperopt_conf.py 2018-06-16 09:09:28 +03:00
pyup-bot
af16830a38 Update requests from 2.19.0 to 2.19.1 2018-06-16 09:09:28 +03:00
pyup-bot
a8d25266f9 Update ccxt from 1.14.196 to 1.14.198 2018-06-16 09:09:28 +03:00
Matthias
b78b9dccc8 Merge pull request #919 from freqtrade/pyup-scheduled-update-2018-06-15
Scheduled daily dependency update on friday
2018-06-15 14:51:54 +02:00
pyup-bot
e8fd11d6ce Update requests from 2.19.0 to 2.19.1 2018-06-15 14:23:08 +02:00
pyup-bot
1e208e39b0 Update ccxt from 1.14.196 to 1.14.198 2018-06-15 14:23:07 +02:00
xmatthias
5c3e37412e update docs 2018-06-14 21:20:16 +02:00
Janne Sinivirta
c731f7dd29 Merge pull request #917 from freqtrade/pyup-scheduled-update-2018-06-14
Scheduled daily dependency update on thursday
2018-06-14 15:42:52 +03:00
pyup-bot
ea805a8fb7 Update ccxt from 1.14.186 to 1.14.196 2018-06-14 14:22:06 +02:00
xmatthias
c0289ad844 use list comprehension to build list 2018-06-13 19:53:12 +02:00
xmatthias
e600be4f56 Reduce force-sell verbosity 2018-06-13 19:44:00 +02:00
Matthias
d7e7ef11f9 Merge pull request #913 from freqtrade/apply-qtpylib-updates
Apply qtpylib upstream changes
2018-06-13 19:34:02 +02:00
gcarq
d684ff5715 drop zlma implementation 2018-06-13 16:20:13 +02:00
ran
6edb25f5c2 fixed heikenashi calculation 2018-06-13 16:17:42 +02:00
ran
e6e5c5daf0 added zlma 2018-06-13 16:16:02 +02:00
ran
61f92b7460 bugfix 2018-06-13 16:13:36 +02:00
Michael Egger
2b74982a1d Merge pull request #877 from freqtrade/feature/improve-rpc
Simplify RPCManager and RPC module to implement other clients
2018-06-13 15:49:49 +02:00
gcarq
46080f5168 define _rpc_reload_conf as private method 2018-06-13 15:29:27 +02:00
Janne Sinivirta
1dcc2de776 Merge pull request #912 from freqtrade/pyup-scheduled-update-2018-06-13
Scheduled daily dependency update on wednesday
2018-06-13 15:44:00 +03:00
pyup-bot
875408215b Update numpy from 1.14.4 to 1.14.5 2018-06-13 14:22:11 +02:00
pyup-bot
038acd3f5e Update pandas from 0.23.0 to 0.23.1 2018-06-13 14:22:09 +02:00
pyup-bot
f404e0f5b3 Update requests from 2.18.4 to 2.19.0 2018-06-13 14:22:08 +02:00
pyup-bot
92b0cbdc19 Update ccxt from 1.14.177 to 1.14.186 2018-06-13 14:22:07 +02:00
gcarq
e14c9e2090 fix potential cleanup issue 2018-06-13 12:21:54 +02:00
gcarq
83eb7a0a9d adjust logging a bit and add some comments 2018-06-13 12:21:54 +02:00
gcarq
6c1bb7983b rpc: make freqtrade a private variable 2018-06-13 12:21:54 +02:00
gcarq
34e10a145c remove Telegram.is_enabled() because RPCManager manages lifecycles 2018-06-13 12:21:54 +02:00
gcarq
3787dad212 don't import python-telegram-bot at runtime if disabled in config 2018-06-13 12:21:54 +02:00
gcarq
4048859912 rpc: remove tuple return madness 2018-06-13 12:21:54 +02:00
gcarq
cddb062db5 save rpc instances only in registered_modules, add some abstract methods 2018-06-13 12:21:54 +02:00
Samuel Husso
13ba68acc6 Merge pull request #908 from freqtrade/fix/plotprofit
fix default datadir not working in plot-script
2018-06-13 08:10:28 +03:00
xmatthias
e22da45474 update documentation with forcesell at the end of the backtest period 2018-06-13 07:00:39 +02:00
xmatthias
6357812743 fix backtest report able 2018-06-13 06:57:49 +02:00
xmatthias
6e68c3b230 fix backtesting.md formatting 2018-06-13 06:52:17 +02:00
xmatthias
0f117d480e improve backtesting-tests
* assert length of result specifically
* add assert for "open_at_end"
2018-06-13 06:42:24 +02:00
xmatthias
8d8e6dcffc Add test for extracted backtest_results test 2018-06-13 06:31:42 +02:00
xmatthias
e3ced7c15e extract export from backtest function 2018-06-12 22:29:30 +02:00
xmatthias
182f4c603b fix plot-script datadir not working 2018-06-12 21:43:14 +02:00
xmatthias
1f6b9c332b fix default datadir not working in plot-script 2018-06-12 21:38:14 +02:00
xmatthias
bfde33c945 Use timestamp() instead of strftime
this will avoid a bug shifting epoch time by 1 hour:
https://stackoverflow.com/questions/11743019/convert-python-datetime-to-epoch-with-strftime
2018-06-12 21:12:55 +02:00
Matthias
bd6ed3ada4 Merge pull request #906 from freqtrade/pyup-scheduled-update-2018-06-12
Scheduled daily dependency update on tuesday
2018-06-12 14:45:24 +02:00
pyup-bot
aa6e276cf9 Update ccxt from 1.14.172 to 1.14.177 2018-06-12 14:22:06 +02:00
Gérald LONLAS
3694499a6a Merge pull request #905 from freqtrade/issue_template
update issue template to include ccxt version
2018-06-11 22:38:58 -07:00
xmatthias
06b71d713c update issue template to include ccxt version 2018-06-12 07:00:58 +02:00
Michael Egger
7141060a2d Merge pull request #903 from freqtrade/fix/downloadscript_noavailable_pair
fix downloadscript crash if a pair is not available
2018-06-12 02:56:19 +02:00
Michael Egger
59a4dffc56 Merge pull request #901 from freqtrade/fix/backtest_abort_no_data
Check if no backtest data is found and fail gracefully
2018-06-12 02:54:58 +02:00
Anton
708320318c Check minimal amount 2018-06-12 01:05:43 +03:00
xmatthias
40746c3fcb fix downloadscript crash if a pair is not available 2018-06-11 21:10:57 +02:00
xmatthias
a0f735d4f2 reduce test-noise 2018-06-11 21:02:24 +02:00
xmatthias
335d1fbbbc Check if no backtest data is found and fail gracefully 2018-06-11 19:50:43 +02:00
Anton
90025d0ac4 Fix check 2018-06-11 16:38:10 +03:00
Anton
ce663f6af5 Merge with develop 2018-06-11 16:25:05 +03:00
Anton
3676015184 Fix check 2018-06-11 16:21:57 +03:00
Samuel Husso
3aff67605e Merge pull request #900 from freqtrade/pyup-scheduled-update-2018-06-11
Scheduled daily dependency update on monday
2018-06-11 15:31:32 +03:00
Samuel Husso
7801688c6e Merge pull request #899 from freqtrade/precommithook
Add note about flake8 pre-commit hooks
2018-06-11 15:24:22 +03:00
pyup-bot
17f3b217de Update ccxt from 1.14.169 to 1.14.172 2018-06-11 14:22:07 +02:00
Janne Sinivirta
d02af07d35 Add not about flake8 pre-commit hooks 2018-06-11 14:55:39 +03:00
Janne Sinivirta
c46e50864b Merge pull request #886 from freqtrade/feature/reload-conf
Reload bot config without restarting
2018-06-11 10:47:00 +03:00
Michael Egger
6c361c190b Merge pull request #897 from freqtrade/fix_backtest_tests
fix backtest tests
2018-06-10 23:13:46 +02:00
xmatthias
12e455cbf5 add buy/sell index to backtest result 2018-06-10 20:52:42 +02:00
xmatthias
a9f3744f1b fix backtest test 2018-06-10 19:46:52 +02:00
Janne Sinivirta
53e1b8c0d5 Merge pull request #895 from freqtrade/pyup-scheduled-update-2018-06-10
Scheduled daily dependency update on sunday
2018-06-10 16:39:07 +03:00
pyup-bot
2ba363684d Update ccxt from 1.14.165 to 1.14.169 2018-06-10 14:22:07 +02:00
xmatthias
9cc087c788 update hyperopt tests to support new structure 2018-06-10 13:56:23 +02:00
xmatthias
4710210cff fix hyperopt to use new backtesting result tuple 2018-06-10 13:56:10 +02:00
xmatthias
27ee8f7360 make flake happy 2018-06-10 13:55:48 +02:00
xmatthias
1cd7ac55a8 Added "left open trades" report 2018-06-10 13:45:16 +02:00
xmatthias
b81588307f Add "open_at_end" parameter 2018-06-10 13:37:53 +02:00
xmatthias
31025216f9 fix type of open/close timestmap 2018-06-10 13:32:07 +02:00
xmatthias
aff1ede46b Fix last backtesting test 2018-06-10 13:25:52 +02:00
xmatthias
322a528c12 fix bug with backtestResult 2018-06-10 13:25:16 +02:00
xmatthias
17c0ceec04 adjust tests for backtestresult type 2018-06-10 13:22:24 +02:00
xmatthias
c9476fade8 adjust tests for forcesell 2018-06-10 13:20:41 +02:00
xmatthias
7b5a2946e5 adjust for forcesell backtesting 2018-06-10 13:19:32 +02:00
xmatthias
9c57d3aa8b add BacktestresultTuple 2018-06-10 13:15:46 +02:00
xmatthias
c1b2e06eda simplify return from _get_sell_trade_entry 2018-06-10 09:07:04 +02:00
xmatthias
3094acc7fb update comment 2018-06-10 08:58:28 +02:00
xmatthias
24a875ed46 remove experimental parameters - they are read by analyze.py anyway 2018-06-09 21:44:57 +02:00
xmatthias
5623ea3ac6 Add forcesell at end of backtest period 2018-06-09 21:44:20 +02:00
Matthias
655155bbab Merge pull request #890 from freqtrade/coveralls-single-execution
avoid running coveralls 4 times
2018-06-09 17:59:04 +02:00
Janne Sinivirta
28e8840456 avoid running coveralls 4 times 2018-06-09 18:52:57 +03:00
Janne Sinivirta
8c73fd6e59 Merge pull request #887 from freqtrade/pyup-scheduled-update-2018-06-09
Scheduled daily dependency update on saturday
2018-06-09 16:02:48 +03:00
pyup-bot
eb58e7cb82 Update ccxt from 1.14.160 to 1.14.165 2018-06-09 14:22:07 +02:00
Janne Sinivirta
8db3dfa8c6 Merge pull request #880 from freqtrade/fix/636
Fixes issue 636
2018-06-09 08:59:12 +03:00
Janne Sinivirta
efd69b2cd5 Merge pull request #883 from freqtrade/fstrings-in-use
fstrings in use
2018-06-09 08:53:54 +03:00
Samuel Husso
38c32f0e10 flake8 fix 2018-06-09 08:40:32 +03:00
Samuel Husso
62b4efb881 freqtradebot: fstrings in use 2018-06-09 08:27:39 +03:00
Samuel Husso
b5c200f6c4 Fiat_converter: fstrings into use 2018-06-09 08:27:39 +03:00
Samuel Husso
18e3090379 Exchange: f-strings into use 2018-06-09 08:27:39 +03:00
Samuel Husso
1e1be6bc3f arguments,configuration: fstring in use 2018-06-09 08:24:45 +03:00
Gerald Lonlas
f0456bb802 Update the README structure 2018-06-08 20:15:52 -07:00
gcarq
61da7f63b2 Merge branch 'develop' of freqtrade into feature/reload-conf 2018-06-09 04:30:23 +02:00
gcarq
0b5d21f32a implement bot reconfiguration and expose it to telegram 2018-06-09 04:29:48 +02:00
gcarq
74db82d759 main: don't touch freqbot state in cleanup()
cleanup() should be only called after the main loop has been exited.
At that point the state shouldn't be modified.
2018-06-09 01:19:42 +02:00
gcarq
5851cc70a7 Merge branch 'develop' of freqtrade into fix/636 2018-06-09 00:37:46 +02:00
Michael Egger
faeda0e70c Merge pull request #878 from freqtrade/fix_timeframe_issue
fix windows-specific init issue with named tuple
2018-06-08 22:44:06 +02:00
Michael Egger
73c5f0ec90 Merge pull request #872 from freqtrade/feature/improve-error-handling
improve error handling
2018-06-08 22:43:37 +02:00
Michael Egger
66f6e71e7e Merge pull request #827 from freqtrade/fix/pylint_and_coverage
Increase code coverage and improve Pylint
2018-06-08 22:32:04 +02:00
xmatthias
cc4b2eef13 mypy - ignore tests folder 2018-06-08 19:58:01 +02:00
xmatthias
8effc5f929 fix windows-specific init issue with named tuple 2018-06-08 19:46:07 +02:00
Samuel Husso
5f93c5e789 Merge pull request #876 from freqtrade/pyup-scheduled-update-2018-06-08
Scheduled daily dependency update on friday
2018-06-08 18:14:43 +03:00
Samuel Husso
980172a55a Merge pull request #865 from freqtrade/partial_candle_removal
Partial candle removal
2018-06-08 18:10:21 +03:00
pyup-bot
760e878dd8 Update ccxt from 1.14.155 to 1.14.160 2018-06-08 14:22:07 +02:00
Samuel Husso
4dbc7abd0f Merge pull request #875 from freqtrade/feat/windows_doc
update windows install documentation
2018-06-08 12:58:26 +03:00
Janne Sinivirta
867faf1c30 Merge pull request #873 from freqtrade/feature/strat_repo_ref
add reference to strategy repository
2018-06-08 12:53:40 +03:00
Matthias
43d19790ae update windows install documentation 2018-06-08 11:23:00 +02:00
Matthias
0bc86e72b3 Add slack reference, fix spelling 2018-06-08 10:57:52 +02:00
Gerald Lonlas
5ca84acb6d Fix Flake8 2018-06-07 23:12:03 -07:00
Samuel Husso
c4af66e312 Merge pull request #874 from freqtrade/local-talib
store ta-lib locally in a zip for Travis
2018-06-08 08:51:39 +03:00
Janne Sinivirta
c37792dbc4 store ta-lib locally in a zip for Travis 2018-06-08 08:15:04 +03:00
Gerald Lonlas
50852136ef Increase FreqtradeBot.get_real_amount() coverage 2018-06-07 22:13:50 -07:00
Gerald Lonlas
20082f52a2 Increase code coverage for FreqtradeBot.process_maybe_execute_sell() 2018-06-07 22:13:50 -07:00
Gerald Lonlas
5ec3eb76eb Cover a edge case of CryptoToFiatConverter::_find_price() 2018-06-07 22:13:50 -07:00
Gerald Lonlas
dfbc94c05b Add missing test for CryptoToFiatConverter::convert_amount() 2018-06-07 22:13:50 -07:00
Gerald Lonlas
81ce7d720d Add missing unit test for Arguments::testdata_dl_options() 2018-06-07 22:13:50 -07:00
Gerald Lonlas
1db0f2bd55 Increase pylint to 10 for freqtrade/arguments.py 2018-06-07 22:13:50 -07:00
xmatthias
9292eb664a add reference to strategy repository
fix markdown to have markdownlint not complain that much
2018-06-08 06:44:59 +02:00
Matthias
8f91eeb195 Merge pull request #870 from freqtrade/feature/increase-main-coverage
add and fix tests for main.py
2018-06-08 06:35:36 +02:00
gcarq
10e12ec1b9 fix flake8 warning 2018-06-08 02:37:12 +02:00
gcarq
61b2373dd1 flush db connection after forcesell 2018-06-08 02:35:10 +02:00
gcarq
7f881cce85 add additional None check for trade.open_order_id 2018-06-08 02:34:44 +02:00
gcarq
bea9a3304e use correct return code on error 2018-06-08 02:01:46 +02:00
gcarq
95d6c9c678 adapt tests 2018-06-08 02:01:38 +02:00
gcarq
a2a1a517da fix flake8 warning 2018-06-08 02:01:18 +02:00
gcarq
27f83b511f raise OperationalException if config is missing 2018-06-08 02:00:42 +02:00
Anton
b1b87731b1 Support case when _get_trade_stake_amount returns None 2018-06-08 00:54:46 +03:00
Anton
b4138f29c8 Merge with develop 2018-06-08 00:29:44 +03:00
gcarq
dd3a53fb5f fix tests for main.py 2018-06-07 22:28:21 +02:00
Matthias
d23bcc435a Merge pull request #864 from freqtrade/feature/overhaul-db-handling
Allow custom sqlite database path
2018-06-07 22:18:10 +02:00
Michael Egger
45eb1b4f0a Merge pull request #869 from freqtrade/feature/profit_rpc
fix /profit percentage calculation
2018-06-07 21:41:32 +02:00
gcarq
d41f71bc34 handle sqlalchemy NoSuchModuleError 2018-06-07 21:35:57 +02:00
xmatthias
f5fe9a4b1c fix rpc tests (add a test with multiple trades
without this, sum/percentage cannot be properly tested.
2018-06-07 20:52:03 +02:00
xmatthias
0e699b87af don't sum percentage, but use mean instead (aligned to backtesting) 2018-06-07 20:43:28 +02:00
gcarq
3f5efef6e5 tests: add proper asserts 2018-06-07 20:41:52 +02:00
gcarq
d4f8704a4c arguments: implement tests for db_url 2018-06-07 20:30:13 +02:00
gcarq
526cb1ea20 fix db-url handling if passed via CLI args 2018-06-07 20:15:31 +02:00
Janne Sinivirta
f5b47fbd86 flake8 fixes 2018-06-07 20:23:09 +03:00
Janne Sinivirta
3cee04fb8c bot should not repaint: do not include last partial candle in analysis 2018-06-07 20:23:09 +03:00
gcarq
ac602ed5a9 persistence: adapt checks to detect in-memory db 2018-06-07 19:10:26 +02:00
Samuel Husso
ad510b8b5f Merge pull request #855 from freqtrade/fix-look-ahead
Avoid look-ahead in backtesting
2018-06-07 20:00:46 +03:00
Samuel Husso
3436af3931 Merge pull request #868 from creslinux/patch-1
plotting.md update.
2018-06-07 19:32:12 +03:00
gcarq
01675f50bf adapt scripts/plot_dataframe to use freqtrade db_url 2018-06-07 18:06:27 +02:00
gcarq
17742df591 Merge branch 'develop' of freqtrade into feature/overhaul-db-handling 2018-06-07 17:33:37 +02:00
gcarq
5b1ff6675f define constants.DEFAULT_DB_DRYRUN_URL and fix StaticPool conditions 2018-06-07 17:29:43 +02:00
creslin
7bcac064c0 Update plotting.md
typo fixed.
2018-06-07 15:18:19 +00:00
Michael Egger
867145cd09 Merge pull request #859 from freqtrade/readd_ticker_caching
Re-add ticker caching for rpc operations
2018-06-07 17:15:59 +02:00
creslin
959a03a6b0 plotting.md update.
include an example or plotting a strategy buy/sell output.
2018-06-07 15:13:55 +00:00
Janne Sinivirta
b4ae5a36a8 use .copy() to avoid Pandas mistake. drop first row because of shifting 2018-06-07 17:29:40 +03:00
Janne Sinivirta
7f8e0ba25f use buy/sell signal from previous candle, not current to avoid seeing to the future 2018-06-07 17:28:40 +03:00
Michael Egger
c75b70463b Merge pull request #852 from freqtrade/timeframe_class
Refactor Timeframe fake-type into NamedTuple
2018-06-07 16:19:44 +02:00
Janne Sinivirta
f9788afbfb Merge pull request #867 from freqtrade/pyup-scheduled-update-2018-06-07
Scheduled daily dependency update on thursday
2018-06-07 17:04:39 +03:00
pyup-bot
7b0a5644a3 Update pytest from 3.6.0 to 3.6.1 2018-06-07 14:22:10 +02:00
pyup-bot
34b5203760 Update numpy from 1.14.3 to 1.14.4 2018-06-07 14:22:08 +02:00
pyup-bot
a2fd70417c Update ccxt from 1.14.121 to 1.14.155 2018-06-07 14:22:07 +02:00
gcarq
c3d0980763 test_persistence: fix reference before assignment 2018-06-07 06:06:21 +02:00
gcarq
4ee5271de7 fix failing dynamic-whitelist test 2018-06-07 05:50:07 +02:00
gcarq
f6ef466876 adapt docs 2018-06-07 05:47:14 +02:00
gcarq
00b646158c update docs 2018-06-07 05:36:39 +02:00
gcarq
c8a43bad67 add db_url to full example config 2018-06-07 05:28:05 +02:00
gcarq
a29ac44d64 adapt tests 2018-06-07 05:27:55 +02:00
gcarq
e2aa78c11b remove obsolete param 2018-06-07 05:27:27 +02:00
gcarq
58a6f21705 remove dry_run_db and replace it with db_url in config 2018-06-07 05:26:39 +02:00
gcarq
8583e89550 persistence: simplify init and pass db_url via config dict 2018-06-07 05:25:53 +02:00
Gérald LONLAS
e8ab754646 Merge pull request #863 from freqtrade/fix/pyup-pin-networkx
exclude networkx from pyup
2018-06-06 18:43:54 -07:00
Michael Egger
5c1ee52815 Merge pull request #861 from freqtrade/pyup-config
Config file for pyup.io
2018-06-07 01:19:21 +02:00
gcarq
02671a7e10 pin networkx with pyup ignore filter 2018-06-07 01:12:46 +02:00
pyup-bot
2ba5e2053a create pyup.io config file 2018-06-07 00:55:09 +02:00
xmatthias
7714490530 Test keyerror exception 2018-06-06 21:24:57 +02:00
xmatthias
4a17671f45 improve log message 2018-06-06 20:30:42 +02:00
xmatthias
a901f21bcd test ticker caching 2018-06-06 20:24:47 +02:00
xmatthias
e690003621 reinstate caching for get_ticker 2018-06-06 20:18:16 +02:00
Matthias
fb49d706d0 Merge pull request #851 from jblestang/update_doc_process_throttle
Update doc process throttle
2018-06-06 00:11:44 +02:00
xmatthias
cac6e0d715 Add docstring to TimeRange class 2018-06-06 00:10:18 +02:00
xmatthias
f37c5b70ba Fix tests - read optional argument 2018-06-05 23:53:49 +02:00
xmatthias
270ccbb0da fix args test 2018-06-05 23:41:50 +02:00
xmatthias
7a34578b4d refactor timerange to named tuple 2018-06-05 23:34:26 +02:00
Anton
12d8a8b1a3 Fix review comments 2018-06-06 00:14:28 +03:00
Janne Sinivirta
7d3eefa97a Merge pull request #838 from freqtrade/fix/plot-scripts
Fix/Improve plot scripts
2018-06-05 15:32:04 +03:00
Jean-Baptiste LE STANG
608fc170d9 fix doc 2018-06-05 13:51:30 +02:00
Jean-Baptiste LE STANG
456d0a050f update doc for process_throttle_secs 2018-06-05 13:49:59 +02:00
Janne Sinivirta
399dd7df95 Merge pull request #849 from freqtrade/readme/fix-links
Docs: point links to freqtrade org
2018-06-05 13:45:54 +03:00
Samuel Husso
7cc36eee0f Docs: point links to freqtrade org 2018-06-05 13:27:24 +03:00
Gerald Lonlas
5024cd52af Update docstring for generate_graph() 2018-06-04 23:49:16 -07:00
Gerald Lonlas
c29c13dfd7 Fix a typo in Arguments() comment 2018-06-04 22:42:24 -07:00
Gerald Lonlas
947462e134 Add back 'import os' in Arguments() 2018-06-04 21:29:53 -07:00
Gerald Lonlas
3778bcda24 Ok! you won Flake8 2018-06-04 21:18:03 -07:00
Gerald Lonlas
1b071b1f4a Add example on how to start the script 2018-06-04 21:18:03 -07:00
Gerald Lonlas
8edcef6d32 Add two params to select what indicators to display 2018-06-04 21:18:03 -07:00
Gerald Lonlas
662436acd2 Fix typo in Argument() 2018-06-04 21:18:03 -07:00
Gerald Lonlas
e16fb45d84 Fix typo, remove Bittrex mention 2018-06-04 21:17:20 -07:00
Gerald Lonlas
1c75bfdddd Add more indicators 2018-06-04 21:17:20 -07:00
Gerald Lonlas
64504e6777 Add support of --refresh-pairs-cached param 2018-06-04 21:17:20 -07:00
Gerald Lonlas
af76d5f0e0 Breakdown the script in functions the improve maintainability 2018-06-04 21:17:20 -07:00
Gerald Lonlas
5683f9e10e Remove hardcoded backtest-result.json in Plot scripts 2018-06-04 21:17:20 -07:00
Matthias
15fb81da92 Merge pull request #844 from creslinux/Constants_usdt
To be able to start with USDT in fiat_display_currency in config.json
2018-06-04 21:56:34 +02:00
creslin
e52ec14588 Update configuration.md
typo, form to from.
2018-06-04 22:19:25 +03:00
creslinux
b13658b319 Updated configuration doc with new fiat values accepted. 2018-06-04 22:17:10 +03:00
creslinux
a44978a068 Per steer from project core member, add other valid coinmarketcap
listed crypto base currencies that are valid during conversion lookup

Here is the test of USDT working:
https://api.coinmarketcap.com/v2/ticker/1027/?convert=USDT&limit=10

CMK page lists: "BTC", "ETH" "XRP", "LTC", and "BCH" as valid.
2018-06-04 21:48:15 +03:00
Matthias
bee2541bd8 Merge pull request #843 from freqtrade/more_timeframes
Add support for more timeframes
2018-06-04 16:23:19 +02:00
creslinux
7c8bf95f8f To be able to start bot with USDT in fiat_display_currency in config.json
There are use case that build the base pair to consider price of whitelist pairs.
On Binance this is USDT not USD.
2018-06-04 16:45:47 +03:00
Janne Sinivirta
7df77b1b28 match timeframes to arguments 2018-06-04 16:35:34 +03:00
Matthias
b995e04daa Merge pull request #841 from freqtrade/choose_tickers_to_download
Choose tickers to download
2018-06-04 14:13:35 +02:00
Janne Sinivirta
0f3dc821f2 add missing timeframes to allowed values 2018-06-04 15:08:45 +03:00
Janne Sinivirta
5ff405b0b0 allow defining of timeframes to download 2018-06-04 15:08:45 +03:00
Samuel Husso
86ae9d25f0 Merge pull request #840 from freqtrade/improve_downloader
Improve ticker downloader
2018-06-04 14:51:02 +03:00
Janne Sinivirta
3321e4cafd travis should run hyperopt and backtesting using tests/testdata tickers 2018-06-04 14:27:42 +03:00
Janne Sinivirta
639b6bc4f6 set and create default datadir based on used exchange 2018-06-04 14:27:42 +03:00
Janne Sinivirta
af1ba1e191 split ugly ternary to regular if 2018-06-04 12:58:35 +03:00
Janne Sinivirta
5c7899ae98 flake8 fix 2018-06-04 12:45:23 +03:00
Janne Sinivirta
d4b431a335 update documentation about download_backtesting_data.py script 2018-06-04 12:37:06 +03:00
Janne Sinivirta
6891054b84 use folder user_data/data/exchangename by default and pick pairs.json from that folder by default 2018-06-04 12:37:06 +03:00
Janne Sinivirta
e10279b7b4 show default exchange in download_backtest_data.py 2018-06-04 11:50:33 +03:00
Janne Sinivirta
a0c79bd727 make --pairs-file required 2018-06-04 11:47:27 +03:00
Janne Sinivirta
4b8f382cfd Merge pull request #839 from freqtrade/fix/incorrect_folder_name_userdata
Fix folder names in custom datadir documentation
2018-06-04 11:24:09 +03:00
Janne Sinivirta
eeda93a359 Fix folder names in custom datadir documentation 2018-06-04 10:04:26 +03:00
Gérald LONLAS
7b79ca3e8f Merge pull request #837 from xmatthias/fix_doc_links
Fix links to point to new repository in owner github account
2018-06-03 18:59:57 -07:00
Anton
3030bf9778 Fix types 2018-06-04 01:52:54 +03:00
Anton
87f750da35 Merge with develop 2018-06-04 01:50:10 +03:00
Anton
daa9c0c026 Fix review comments 2018-06-04 01:48:26 +03:00
xmatthias
5ef2654eb4 replace references to old url
replace garq with freqtrade
2018-06-03 23:07:00 +02:00
xmatthias
26120ff675 remove unnecessary .gitkeep 2018-06-03 23:06:37 +02:00
Gérald LONLAS
e453dab4a3 Merge pull request #831 from xmatthias/backtest_export_filename
allow export of backtesting-results to different files
2018-06-03 13:12:38 -07:00
xmatthias
482d063638 update documentation for --export-filename 2018-06-03 19:41:34 +02:00
Janne Sinivirta
2f3b0cd422 Merge pull request #835 from gcarq/pyup-update-ccxt-1.14.120-to-1.14.121
Update ccxt to 1.14.121
2018-06-03 20:40:22 +03:00
xmatthias
e3227a741c add --export-filename for backtesting 2018-06-03 19:36:53 +02:00
pyup-bot
4eb8295955 Update ccxt from 1.14.120 to 1.14.121 2018-06-03 19:27:08 +02:00
Samuel Husso
bdb25bbcbc Merge pull request #834 from gcarq/feature/__main__
Add __main__.py to improve how to launch the bot
2018-06-03 19:28:23 +03:00
Gerald Lonlas
43696eff5c Add __main__.py to improve how to launch the bot 2018-06-03 08:57:13 -07:00
Michael Egger
c6b93f8fe5 Merge pull request #833 from gcarq/fix/backtesting_doc
Update Backtesting/Hyperopt usage documentation
2018-06-03 17:43:41 +02:00
Gerald Lonlas
d3d62e90d3 Update Backtesting/Hyperopt usage documentation 2018-06-03 08:36:01 -07:00
Janne Sinivirta
20815771ab Merge pull request #817 from gcarq/feature/gdax
Enable Backtesting with GDAX and allow trading with EUR/USD
2018-06-03 17:49:20 +03:00
Janne Sinivirta
b6754601ef Merge pull request #832 from xmatthias/contrib_document
update contributing document to include mypy
2018-06-03 17:43:59 +03:00
xmatthias
0f352a4b5c update contributing document to include mypy 2018-06-03 15:14:51 +02:00
Samuel Husso
7d6b11cb10 Merge pull request #830 from xmatthias/refactor_fiat_list
Refactor fiat-list to constants
2018-06-03 15:57:23 +03:00
xmatthias
3a158faa30 Refactor fiat-list to constants 2018-06-03 13:47:36 +02:00
Matthias
fff7ec1dab Merge pull request #808 from xmatthias/mypy_typecheck
add mypy typechecking
2018-06-03 10:43:55 +02:00
xmatthias
50fc5f91ca Merge branch 'develop' into mypy_typecheck 2018-06-03 10:35:56 +02:00
Samuel Husso
ec7c11513e Merge pull request #829 from gcarq/pyup-update-ccxt-1.14.119-to-1.14.120
Update ccxt to 1.14.120
2018-06-03 11:31:50 +03:00
pyup-bot
cfb06ceb58 Update ccxt from 1.14.119 to 1.14.120 2018-06-03 10:12:07 +02:00
Gerald Lonlas
e8a59f4c20 Add a test to check the behavior when converting two FIAT 2018-06-03 00:13:48 -07:00
Gerald Lonlas
638d98735f Allow fiat_convert to use same symbol for Crypto and FIAT 2018-06-03 00:13:48 -07:00
Gerald Lonlas
c9e49ed7b4 Sort ticker_history
CCXT does not sort the ticker history from exchanges.
Bittrex and Binance are sorted ASC (oldest first, newest last) when
GDAX is sorted DESC (newest first, oldest last).

Because of that the get_ticker_history() fall in a very long loop
when the tickers are sorted DESC. Means it downloads more than
needed.

This commit enable exhanges like GDAX and unify the ticker_history
list across all exchanges.
2018-06-03 00:13:48 -07:00
Gerald Lonlas
acbfe91f13 Allow EUR / USD as stake_currency
It will enable to trade with FIAT on exhanges like GDAX or Kraken.
2018-06-03 00:13:48 -07:00
Janne Sinivirta
7edafbb772 Merge pull request #823 from creslinux/timerange_unixtime_argument
Timerange unixtime argument
2018-06-03 07:22:41 +03:00
Janne Sinivirta
a657e3d24a Merge pull request #826 from gcarq/fix/hyperopt-stake_currency
Fix stake_currency returned by Hyperopt  …
2018-06-03 07:19:24 +03:00
Janne Sinivirta
2cd8782a88 Merge pull request #825 from gcarq/fix/hyperopt-in-progress
Fix the in-progress dot that does not show up during a Hyperopt run
2018-06-03 07:16:39 +03:00
Gerald Lonlas
fe8ff1b929 Fix stake_currency return by Hyperopt
Hyperopt had BTC hard coded in the result. This commit  will display
the real stake_currency used.

If you used `"stake_currency": "USDT",` in your config file.
Before this commit you saw a message like:
"2 trades. Avg profit  0.13%. Total profit  0.00002651 BTC (0.0027Σ%). Avg duration 142.5 mins."

Now with the commit, we fix the wrong BTC currency:
"2 trades. Avg profit  0.13%. Total profit  0.00002651 USDT (0.0027Σ%). Avg duration 142.5 mins."
2018-06-02 14:07:31 -07:00
Gerald Lonlas
127cf5d619 Backtesting: Add the Interval required when data is missing
Change the message:
"No data for pair ETH/BTC, use --refresh-pairs-cached to download the data"
for:
"No data for pair: "ETH/BTC", Interval: 5m. Use --refresh-pairs-cached to download the data"

The message structure is unified with the download message:
"Download the pair: "ETH/BTC", Interval: 5m"
2018-06-02 13:55:05 -07:00
Gérald LONLAS
5e99df1759 Merge pull request #824 from xmatthias/rymdluo-patch-1
Make backtesting report markdown shareable (resubmit)
2018-06-02 13:05:11 -07:00
creslinux
94e586c049 Added unit test to check posix time arguments passed to timerange
Here is the pass report:
freqtrade_new creslin$ pytest freqtrade/tests/test_arguments.py
==================================================================== test session starts =====================================================================
platform darwin -- Python 3.6.5, pytest-3.6.0, py-1.5.3, pluggy-0.6.0
rootdir: /Users/creslin/PycharmProjects/freqtrade_new, inifile:
plugins: mock-1.10.0, cov-2.5.1
collected 19 items

freqtrade/tests/test_arguments.py ...................                                                                                                  [100%]

================================================================= 19 passed in 2.37 seconds ==================================================================
2018-06-02 22:46:54 +03:00
Gerald Lonlas
dc65753a64 Fix the in-progress dot that does not show up during a Hyperopt run 2018-06-02 12:35:07 -07:00
creslin
43ba02afc6 Per feed back, kept the stype as date.
Use a tuple to keep as epoch int or process via arrow to timestamp.

I'll look at the test file also.
2018-06-02 21:59:18 +03:00
xmatthias
9537f17dd4 Fix test 2018-06-02 20:06:29 +02:00
Raymond Luo
2791d543ea Make backtesting report markdown shareable
Small tweak to make the backtesting report markdown ready and much easier to share reports on many markdown publishing tools and editors that already support Markdown Extra with just a copy and paste

Example:
![Example](https://i.imgur.com/HXlNkfm.png)
2018-06-02 19:52:16 +02:00
creslin
9dbe5fdb85 Update back testing document to include example using Posix timestamps
as timerange

e.g
--timerange=1527595200-1527618600
2018-06-02 19:49:23 +03:00
creslin
6ca375a397 Extend timerange to accept unix timestamps.
This gives greater granularity over backtest, parsing tickerfiles.

Example runs using date and unix time below.

/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/bin/python3.6 /Users/creslin/PycharmProjects/freqtrade/scripts/report_correlation.py --timerange=20180528-20180529
2018-06-02 18:44:58,829 - freqtrade.configuration - INFO - Log level set to INFO
2018-06-02 18:44:58,830 - freqtrade.configuration - INFO - Using max_open_trades: 200 ...
2018-06-02 18:44:58,831 - freqtrade.configuration - INFO - Parameter --timerange detected: 20180528-20180529 ...
2018-06-02 18:44:58,831 - freqtrade.configuration - INFO - Parameter --datadir detected: freqtrade/tests/testdata ...
   BasePair      Pair  Correlation  BTC % Change  Pair % USD Ch  Pair % BTC Ch  Gain % on BTC        Start         Stop  BTC Volume
1  BTC_USDT   ETC_USD        0.965        -2.942         -4.070         -1.163      -1.128585  05-28 00:00  05-29 00:00      335.19
0  BTC_USDT   SNT_USD        0.943        -2.942         -5.857         -3.004      -2.915585  05-28 00:00  05-29 00:00      496.01
3  BTC_USDT  DASH_USD        0.902        -2.942         -9.034         -6.277      -6.092432  05-28 00:00  05-29 00:00      751.41
2  BTC_USDT   MTH_USD        0.954        -2.942         -9.290         -6.541      -6.348708  05-28 00:00  05-29 00:00       23.00
4  BTC_USDT   TRX_USD        0.951        -2.942        -13.647        -11.029     -10.704957  05-28 00:00  05-29 00:00    14544.57

Process finished with exit code 0

/usr/local/Cellar/python3/3.6.2/Frameworks/Python.framework/Versions/3.6/bin/python3.6 /Users/creslin/PycharmProjects/freqtrade/scripts/report_correlation.py --timerange=1527595200-1527618600
2018-06-02 18:47:40,382 - freqtrade.configuration - INFO - Log level set to INFO
2018-06-02 18:47:40,382 - freqtrade.configuration - INFO - Using max_open_trades: 200 ...
2018-06-02 18:47:40,383 - freqtrade.configuration - INFO - Parameter --timerange detected: 1527595200-1527618600 ...
2018-06-02 18:47:40,383 - freqtrade.configuration - INFO - Parameter --datadir detected: freqtrade/tests/testdata ...
   BasePair      Pair  Correlation  BTC % Change  Pair % USD Ch  Pair % BTC Ch  Gain % on BTC        Start         Stop  BTC Volume
0  BTC_USDT   SNT_USD        0.680           NaN            NaN            NaN            NaN  05-29 12:00  05-29 18:30    68866.30
1  BTC_USDT   ETC_USD        0.857           NaN            NaN            NaN            NaN  05-29 12:00  05-29 18:30   227514.17
2  BTC_USDT   MTH_USD        0.790           NaN            NaN            NaN            NaN  05-29 12:00  05-29 18:30    12103.96
3  BTC_USDT  DASH_USD        0.862           NaN            NaN            NaN            NaN  05-29 12:00  05-29 18:30    72982.78
4  BTC_USDT   TRX_USD        0.178           NaN            NaN            NaN            NaN  05-29 12:00  05-29 18:30  1258316.95

Process finished with exit code 0
2018-06-02 19:45:08 +03:00
Matthias
81bb128cf7 Merge pull request #822 from gcarq/fix/misleading_log
change misleading logging for datadir
2018-06-02 14:50:27 +02:00
xmatthias
a8bf5092e8 add ignore explanation 2018-06-02 14:18:57 +02:00
xmatthias
f88729f0e8 add ignore comment 2018-06-02 14:14:28 +02:00
xmatthias
3447e4bb97 comment on ignore hint 2018-06-02 14:13:17 +02:00
xmatthias
884395415f remove type:ignore 2018-06-02 14:10:15 +02:00
xmatthias
0007002c80 fix test failure 2018-06-02 14:07:54 +02:00
xmatthias
0a595190a3 fix last typechecks 2018-06-02 13:59:35 +02:00
xmatthias
32300f6d5f don't initialize with None where it's not necessary 2018-06-02 13:55:06 +02:00
xmatthias
d9e951447f remove _init function in backtesting (and according test) 2018-06-02 13:54:22 +02:00
xmatthias
6fc21e30e5 remove unused import 2018-06-02 13:52:55 +02:00
xmatthias
6106822d10 typing 2018-06-02 13:44:41 +02:00
xmatthias
4a322abd4d Typecheck improvements 2018-06-02 13:44:05 +02:00
Janne Sinivirta
52309cc292 Merge pull request #819 from gcarq/pyup-update-ccxt-1.14.96-to-1.14.119
Update ccxt to 1.14.119
2018-06-02 11:57:58 +03:00
Janne Sinivirta
b5c41ca0fc Merge pull request #820 from gcarq/fix/backtesting_hint
Fix wrong hint '--update-pairs-cached' from Backtesting/Hyperopt
2018-06-02 11:39:09 +03:00
Janne Sinivirta
a82a31341b change misleading logging for datadir 2018-06-02 11:32:05 +03:00
Gérald LONLAS
0980e7e82d Merge pull request #766 from pan-long/forcesell-amount
Sell filled amount or an open limit buy order in forcesell.
2018-06-01 19:51:38 -07:00
Gérald LONLAS
41efe99770 Merge pull request #786 from gcarq/fix/setup_script
Update setup.sh
2018-06-01 19:48:29 -07:00
Gerald Lonlas
792dd556a1 Fix wrong hint '--update-pairs-cached' from Backtesting/Hyperopt 2018-06-01 19:46:53 -07:00
pyup-bot
b731a65c75 Update ccxt from 1.14.96 to 1.14.119 2018-06-02 04:27:04 +02:00
xmatthias
e28973c50a fix flake8 2018-05-31 22:17:46 +02:00
xmatthias
633620a5e9 exclude .mypy_cache 2018-05-31 22:15:18 +02:00
xmatthias
41a47df93f setup travis to check mypy 2018-05-31 22:09:31 +02:00
xmatthias
3fb1dd02f1 add typehints and type: ignores 2018-05-31 22:00:46 +02:00
xmatthias
cf34b84cf1 add attributes with typehints 2018-05-31 21:59:22 +02:00
xmatthias
f4f821e88e add typehints 2018-05-31 21:44:18 +02:00
xmatthias
c0cef7250d typing - avoid variable reuse with differen ttype 2018-05-31 21:22:46 +02:00
xmatthias
2976a50c58 fix typing 2018-05-31 21:10:15 +02:00
xmatthias
69006b8fe8 flake8 2018-05-31 21:08:26 +02:00
xmatthias
4eb55acdbc fix typing 2018-05-31 21:04:10 +02:00
xmatthias
1352f135d0 typing 2018-05-31 20:55:45 +02:00
xmatthias
0d251cbfdd rpc type hints 2018-05-31 20:55:26 +02:00
xmatthias
4733aad7ff mypy_typing 2018-05-31 20:54:37 +02:00
xmatthias
48516e6e1e Add typehint 2018-05-31 20:41:05 +02:00
xmatthias
45909af7e0 type anotation fixes 2018-05-30 22:38:09 +02:00
xmatthias
88755fcded fix typing 2018-05-30 22:09:20 +02:00
xmatthias
0d6dffdc7e fix typehinting 2018-05-30 22:09:03 +02:00
xmatthias
9aa468adda fix for typehint 2018-05-30 22:01:29 +02:00
Janne Sinivirta
52386d8153 Merge pull request #793 from gcarq/pyup-update-ccxt-1.14.73-to-1.14.96
Update ccxt to 1.14.96
2018-05-30 21:40:32 +03:00
pyup-bot
b7e0466d7c Update ccxt from 1.14.73 to 1.14.96 2018-05-30 18:42:00 +02:00
Samuel Husso
f91de3c10e Merge pull request #788 from gcarq/fix/doc_configuration
Update Readme and documentation
2018-05-30 08:53:57 +03:00
Gerald Lonlas
4329c15a9b Doc: Add Buzz/trendy word 2018-05-29 22:38:48 -07:00
Gerald Lonlas
963d2a8368 Doc: update bot usage 2018-05-29 22:24:13 -07:00
Gerald Lonlas
d9eddfb1ee Doc: Update the exchanges supported 2018-05-29 22:21:29 -07:00
Gerald Lonlas
f59f534c64 Setup.sh: fix Python3.6 when broken on macOS 2018-05-29 20:49:37 -07:00
Gerald Lonlas
5a4eb2cbf2 Setup.sh: make message format consistent 2018-05-29 20:48:34 -07:00
Samuel Husso
c471ccb2db Merge pull request #734 from arudov/fix/pair-downloads
Do not download pairs if --refresh-pairs-cached isn't set
2018-05-29 08:05:10 +03:00
Samuel Husso
656be523bc Merge pull request #779 from gcarq/pyup-update-sqlalchemy-1.2.7-to-1.2.8
Update sqlalchemy to 1.2.8
2018-05-29 08:03:58 +03:00
pyup-bot
9cd7749867 Update sqlalchemy from 1.2.7 to 1.2.8 2018-05-28 22:14:50 +02:00
Samuel Husso
1845e5d7ca Merge pull request #772 from gcarq/pyup-update-ccxt-1.14.62-to-1.14.73
Update ccxt to 1.14.73
2018-05-27 10:23:42 +03:00
Samuel Husso
9639a3805d Merge pull request #771 from creslinux/develop
Correct instructions in backtesting.md
2018-05-27 10:23:29 +03:00
Samuel Husso
bc88fbf948 Merge pull request #767 from xmatthias/ccxt_loglevel
set ccxt loglevel to info
2018-05-27 10:22:20 +03:00
pyup-bot
94c1a6f2a6 Update ccxt from 1.14.62 to 1.14.73 2018-05-26 23:41:52 +02:00
creslin
280e8b3208 Update backtesting.md - correct instructions
Correct instructions for calling a custom strategy file
To paraphrase the change:

Prior - to call a custom strategy -s the strategy file name within users_data/strategies/ directory
After - to call a custom strategy -s the class name within the strategy within users_data/strategies/ directory
2018-05-26 20:14:33 +03:00
creslin
607c895065 Update backtesting.md: how to call a custom strat
Corrected instructions, to paraphrase the PR 
prior - to call a custom strategy -s the custom strategy file name in user_data/strategies 
after - to call a custom strategy -s the class name within the custom strategy file name in user_data/strategies
2018-05-26 20:09:20 +03:00
Pan Long
a98fcee4f9 Sell filled amount or an open limit buy order in forcesell.
Currently forcesell only cancels an open limit buy order and doesn't sell the filled amount.

After this change, forcesell will also update trade's amount to filled amount and sell the filled amount.
2018-05-26 09:55:31 +08:00
xmatthias
1ba5c5d9c6 set ccxt loglevel to info 2018-05-25 21:23:15 +02:00
Anton
3427c7eb54 Use constants 2018-05-25 17:04:08 +03:00
Anton
cf5d691950 Clean the tests 2018-05-25 00:46:08 +03:00
Janne Sinivirta
4e0b095f2b Merge pull request #756 from gcarq/pyup-update-ccxt-1.14.27-to-1.14.62
Update ccxt to 1.14.62
2018-05-24 10:59:40 +03:00
Janne Sinivirta
0837f3f9f3 Merge pull request #733 from xmatthias/fix_fiat_init
Fix fiat initialization
2018-05-24 10:54:31 +03:00
pyup-bot
bad5d57d71 Update ccxt from 1.14.27 to 1.14.62 2018-05-24 08:26:46 +02:00
Samuel Husso
620c7e8312 Merge pull request #748 from gcarq/pyup-update-pytest-3.5.1-to-3.6.0
Update pytest to 3.6.0
2018-05-24 09:01:31 +03:00
pyup-bot
af0b1e806f Update pytest from 3.5.1 to 3.6.0 2018-05-23 15:06:26 +02:00
Samuel Husso
cf522d1df2 Merge pull request #747 from creslinux/patch-1
OSX docker start cmd updated
2018-05-23 16:06:18 +03:00
creslin
318c973461 Update to installation.md
Added link to Docker issue  on OSX with greater detail of the problem and work-around.
2018-05-23 15:20:16 +03:00
creslin
34e78a7400 OSX docker start cmd updated
New versions of Docker will not start in OSX using the cmd in these instructions as /etc/localtime cannot be mounted. 
The change provides an alternate command that does work. 
`docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade`

More info is in this thread: 
https://github.com/docker/for-mac/issues/2396
2018-05-23 13:17:35 +03:00
Anton
9be98cd8f7 Add ability to set unlimited stake_amount 2018-05-23 13:15:03 +03:00
Samuel Husso
e267b84510 Merge pull request #741 from pan-long/setup-defaults
Auto apply default values in setup.
2018-05-23 10:24:22 +03:00
Pan Long
c7ef69f4eb Auto apply default values in setup.
Before this commit, during setup, even a default value is displayed for some config, if user doesn't enter anything, an empty value is applied.

After this commit, if user doesn't enter anything for a config with default value, the default value will be applied.
2018-05-22 22:09:52 +08:00
Anton
8c22cfce37 Fix tests; fix codestyle 2018-05-21 23:15:01 +03:00
Anton
e1cb0dbf28 Do not try to redownload pair data if --refresh-pairs-cached is not set 2018-05-21 22:31:08 +03:00
xmatthias
e2efd7c6ec add test to verify network exception is cought on init of coinmarketcap 2018-05-21 20:03:25 +02:00
xmatthias
56e697acf5 Fix error initializing coinmarketcap 2018-05-21 20:01:41 +02:00
Michael Egger
13d6297b9f Merge pull request #711 from gcarq/pyup-update-ccxt-1.14.24-to-1.14.27
Update ccxt to 1.14.27
2018-05-20 10:31:27 +02:00
pyup-bot
65c069dd9f Update ccxt from 1.14.24 to 1.14.27 2018-05-20 06:41:38 +02:00
Samuel Husso
b0536dba0b Merge pull request #709 from gcarq/pyup-update-ccxt-1.14.10-to-1.14.24
Update ccxt to 1.14.24
2018-05-19 09:15:02 +03:00
peterkorodi
0c051b1b7a Make plot_dataframe able to show trades stored in database. (#692)
* Show trades stored in db on the graph
2018-05-19 09:14:42 +03:00
pyup-bot
16eb793081 Update ccxt from 1.14.10 to 1.14.24 2018-05-19 06:56:37 +02:00
Samuel Husso
1cc132afe2 Merge pull request #695 from gcarq/pyup-update-ccxt-1.13.148-to-1.14.10
Update ccxt to 1.14.10
2018-05-17 08:23:32 +03:00
Samuel Husso
d985405fe7 Merge pull request #683 from xmatthias/fix_get_real_amount
Fix get real amount
2018-05-17 08:22:34 +03:00
pyup-bot
e88fabe1d6 Update ccxt from 1.13.148 to 1.14.10 2018-05-17 00:26:32 +02:00
Samuel Husso
7f1f1ec1ad Merge pull request #688 from gcarq/pyup-update-pandas-0.22.0-to-0.23.0
Update pandas to 0.23.0
2018-05-16 08:37:38 +03:00
pyup-bot
8094f84efe Update pandas from 0.22.0 to 0.23.0 2018-05-16 05:16:24 +02:00
Matthias Voppichler
ef78f2f03a Add test for invalid order_fee dict 2018-05-15 20:13:43 +02:00
Matthias Voppichler
a1fa688da0 Add tests for the new scenario 2018-05-15 19:49:47 +02:00
Matthias Voppichler
263bf918b1 Fix bug pointed out in #679 2018-05-15 19:49:28 +02:00
Samuel Husso
58a2af8d80 Merge pull request #678 from arudov/fix/get-balance
Fixed bot crash while requesting the current balance
2018-05-15 18:10:02 +03:00
Samuel Husso
594b541f34 Merge pull request #680 from gcarq/pyup-update-ccxt-1.13.147-to-1.13.148
Update ccxt to 1.13.148
2018-05-15 18:07:16 +03:00
pyup-bot
cc3e4e9aa7 Update ccxt from 1.13.147 to 1.13.148 2018-05-15 16:41:31 +02:00
Janne Sinivirta
d74a0f0526 Merge pull request #677 from gcarq/pyup-update-coinmarketcap-5.0.1-to-5.0.3
Update coinmarketcap to 5.0.3
2018-05-15 17:39:37 +03:00
Anton
d112d90e8e Make telegram message beautiful 2018-05-15 13:37:34 +03:00
Michael Egger
2383a83c2d Merge pull request #675 from gcarq/pyup-update-ccxt-1.13.142-to-1.13.147
Update ccxt to 1.13.147
2018-05-15 12:36:27 +02:00
pyup-bot
c2245362da Update coinmarketcap from 5.0.1 to 5.0.3 2018-05-15 08:41:22 +02:00
pyup-bot
c96f912043 Update ccxt from 1.13.142 to 1.13.147 2018-05-15 01:11:29 +02:00
Anton
f175f48418 Fix get balance functionality 2018-05-15 00:31:56 +03:00
Janne Sinivirta
6cc8017943 Merge pull request #670 from gcarq/flakify-scripts
Scripts: fix syntax errors and flake8ify
2018-05-14 08:42:11 +03:00
Samuel Husso
e0bd45efab Scripts: fix syntax errors and flake8ify 2018-05-14 08:08:40 +03:00
Samuel Husso
f80864b5bc Merge pull request #668 from gcarq/pyup-update-ccxt-1.13.138-to-1.13.142
Update ccxt to 1.13.142
2018-05-14 07:14:18 +03:00
pyup-bot
9a09e6b815 Update ccxt from 1.13.138 to 1.13.142 2018-05-14 03:26:26 +02:00
Michael Egger
91f90920c2 Merge pull request #665 from xmatthias/fix_fiat_convert
Fix fiat convert
2018-05-13 22:37:01 +02:00
Matthias Voppichler
8549201502 add test for new fiat_convert logic 2018-05-13 20:46:02 +02:00
Samuel Husso
0665a23b0f Merge pull request #663 from gcarq/pyup-update-ccxt-1.13.136-to-1.13.138
Update ccxt to 1.13.138
2018-05-13 21:27:01 +03:00
Matthias Voppichler
b1c53ec656 refactor "patch_coinmarketcap" to conftest"
add patch_coinmarketcap to get_patched_freqtradebot
2018-05-13 20:04:40 +02:00
Matthias Voppichler
790f35a5c8 fix test which resets singleton without reinstating it 2018-05-13 20:03:54 +02:00
Matthias Voppichler
3246c60472 Fix coinmarketcap ticker 2018-05-13 20:00:38 +02:00
Matthias Voppichler
57fc9df5f3 Fix typo 2018-05-13 19:54:19 +02:00
Matthias Voppichler
144be37a9a Convert ID to string 2018-05-13 19:53:23 +02:00
Matthias Voppichler
9b8f90dc9f log error in find_price 2018-05-13 19:50:04 +02:00
Matthias Voppichler
d07491ceb2 Dynamically load cryptomap 2018-05-13 19:46:08 +02:00
pyup-bot
14c140d242 Update ccxt from 1.13.136 to 1.13.138 2018-05-13 16:26:25 +02:00
Michael Egger
263d34ae82 Merge pull request #660 from xmatthias/fix_hyperopt_testfluke
Fix testfluke in hyperopt
2018-05-13 14:51:27 +02:00
Matthias Voppichler
8f17b11610 Fix testfluke in hyperopt 2018-05-13 13:38:29 +02:00
Samuel Husso
177962fa05 Merge pull request #657 from gcarq/pyup-update-ccxt-1.13.133-to-1.13.136
Update ccxt to 1.13.136
2018-05-13 10:23:34 +03:00
pyup-bot
d51ac94662 Update ccxt from 1.13.133 to 1.13.136 2018-05-13 05:41:24 +02:00
Samuel Husso
40dfe4b3a9 Merge pull request #655 from xmatthias/dev_reduce_verbosity
Reduce verbosity of get_ticker_history
2018-05-12 22:20:08 +03:00
Matthias Voppichler
8b098859f4 Reduce verbosity of get_ticker_history 2018-05-12 20:15:59 +02:00
Samuel Husso
72a2c37769 Merge pull request #654 from gcarq/pyup-update-cachetools-2.0.1-to-2.1.0
Update cachetools to 2.1.0
2018-05-12 20:42:15 +03:00
pyup-bot
bc25007fef Update cachetools from 2.0.1 to 2.1.0 2018-05-12 18:45:18 +02:00
Michael Egger
1e119013c8 Merge pull request #653 from gcarq/pyup-update-ccxt-1.11.149-to-1.13.133
Update ccxt to 1.13.133
2018-05-12 14:40:34 +02:00
pyup-bot
189873f9d4 Update ccxt from 1.11.149 to 1.13.133 2018-05-12 14:04:16 +02:00
Michael Egger
5b25ed99ac Merge pull request #652 from gcarq/feat/objectify-ccxt
CCXT into use
2018-05-12 14:04:06 +02:00
Michael Egger
edd840ac35 Merge pull request #640 from xmatthias/ccxt-obj-slippage
[cxxt][2/2] Add columns for slippage detection
2018-05-12 13:56:15 +02:00
Matthias Voppichler
58425993da Adapt tests to verify pair-conversion and exchange conversion 2018-05-12 13:39:29 +02:00
Matthias Voppichler
e3ae1c6c2f Convert exchange-name to new format 2018-05-12 13:39:16 +02:00
Matthias Voppichler
40c581e5a8 Convert pair-format to new format 2018-05-12 13:37:42 +02:00
Matthias Voppichler
631081a2b2 Add additional tests 2018-05-12 10:37:17 +02:00
Matthias Voppichler
8e3ff8235f add explaining comments 2018-05-12 10:31:24 +02:00
Matthias Voppichler
ada98abfee fix flake 2018-05-12 10:30:30 +02:00
Matthias Voppichler
49266fc4b8 Add migration test 2018-05-12 10:29:26 +02:00
Matthias Voppichler
f5ff6ceead Rename instead of drop/create 2018-05-12 10:29:10 +02:00
Matthias Voppichler
81ee6f8265 Update sql docs to new schema 2018-05-12 10:19:52 +02:00
Matthias Voppichler
ab4e2bd5a9 Fix migrate script 2018-05-12 10:04:41 +02:00
Samuel Husso
01b6a0eb53 Freqtrade: ccxt release shall be called 0.17.0 2018-05-12 09:57:10 +03:00
Samuel Husso
b55822ad30 telegram: document proxy usage without code changes per gcarq's
comment in #609
2018-05-09 09:22:01 +03:00
Samuel Husso
7552c912a2 config.json.example: add ticker_interval 2018-05-09 09:15:09 +03:00
Michael Egger
1dbdb880e6 Merge pull request #637 from arudov/fix/dl-testdata-period2
Time-range download of backtesting data
2018-05-07 17:19:54 +02:00
Matthias Voppichler
ccf1c894b4 Inital try mirate 2018-05-06 09:09:53 +02:00
Matthias Voppichler
d3fb2e4516 Add open_rate_requested and close_rate_requested for slippage detection 2018-05-05 12:57:07 +02:00
Anton
932b65da27 Fix test_optimize.py 2018-05-04 13:59:50 +03:00
Anton
2bfce64e6a Fix conflicts 2018-05-04 13:38:51 +03:00
gcarq
43fd9b37df fix 'max_open_trades must be greater than 0' regression 2018-05-03 10:48:25 +02:00
Anton
ceeb98dda9 Fix conflicts 2018-05-03 11:16:29 +03:00
gcarq
a5c1547251 user_data: change ticker_interval to new format 2018-05-02 22:56:29 +02:00
gcarq
306885e174 Merge branch 'develop' into feat/objectify-ccxt 2018-05-02 22:49:55 +02:00
Michael Egger
90a107393a Merge pull request #622 from gcarq/fix/dl-testdata
fix download testdata
2018-05-02 22:06:43 +02:00
Michael Egger
c72d4665a1 Merge pull request #619 from gcarq/feature/catch-exchange-errors
granular exception handling and retrying mechanism for ccxt
2018-05-02 20:13:16 +02:00
gcarq
a76ed88496 Merge branch 'feat/objectify-ccxt' into feature/catch-exchange-errors 2018-05-02 20:03:13 +02:00
Anton
24ab1b5be5 Fix review comments, documenation update 2018-05-01 00:27:05 +03:00
Samuel Husso
842b0c2270 Exchange: fix missing comma and typehinting per PR comments 2018-04-29 18:55:43 +03:00
Anton
a127e1db07 Fix case with empty dict 2018-04-28 01:40:48 +03:00
Anton
2267a420a4 Fix codestyle 2018-04-28 00:30:42 +03:00
Anton
82ea56c8fd Fix review comments. Add support of datetime timeganges 2018-04-28 00:16:34 +03:00
Michael Egger
ecaf6b763c Merge pull request #623 from xmatthias/cxxt_obj_sellfix
[cxxt][1/2] fix fee calculation in binance
2018-04-26 19:58:24 +02:00
Matthias Voppichler
0987af910e remove indicator name from comment 2018-04-25 20:03:32 +02:00
Matthias Voppichler
2e1124af1a remove unnecessary .keys() 2018-04-25 14:00:25 +02:00
Anton
2fe7812e20 Fix codestyle 2018-04-25 10:32:58 +03:00
Matthias Voppichler
8bd9ed1543 fix flake8 2018-04-25 09:13:56 +02:00
Matthias Voppichler
72c17e29c0 Add test for "no trades found" case 2018-04-25 09:08:02 +02:00
Matthias Voppichler
483415cd65 Add fee entry to DRY_ORDER dict as defined by ccxt 2018-04-25 09:03:32 +02:00
Matthias Voppichler
98669a3d62 remove duplicate log entry, fix key-error 2018-04-25 09:01:21 +02:00
Matthias Voppichler
9c2115c917 refactor get_real_amount 2018-04-25 08:52:08 +02:00
Matthias Voppichler
f6ecd8e514 Add pytest fixture for real_amount test 2018-04-25 08:51:31 +02:00
Anton
6675120324 Add time range support to download_backtest_data 2018-04-25 02:11:07 +03:00
Matthias Voppichler
ab6589d573 Fix comment and improve log message 2018-04-24 19:43:08 +02:00
Matthias Voppichler
9e94778fd7 simplify check for presence of list 2018-04-24 19:42:41 +02:00
Matthias Voppichler
2968347062 fix flake8 2018-04-23 20:32:46 +02:00
Matthias Voppichler
9450b76414 improve style of import in test 2018-04-23 20:08:58 +02:00
Matthias Voppichler
d2608cbf13 improve check when not to run 2018-04-23 20:06:00 +02:00
Matthias Voppichler
f580fbb91d remove maybe_update_amount and tests 2018-04-23 20:03:10 +02:00
gcarq
9b0fbbdc14 cancel_order: pass all positional arguments 2018-04-23 16:58:52 +02:00
gcarq
aa213a3640 cancel_order: handle InvalidOrder exception 2018-04-23 16:58:32 +02:00
gcarq
baeeaa777d get_balance: handle case if currency is not in response 2018-04-23 16:57:18 +02:00
gcarq
20af4bae7c retrier: raise initial exception instead of OperationalException 2018-04-23 16:56:35 +02:00
gcarq
5baab91bb5 catch TemporaryError for buy/sell in _process() 2018-04-22 20:28:39 +02:00
gcarq
4c49229b77 catch DependencyExceptions while selling 2018-04-22 20:27:34 +02:00
Matthias Voppichler
93a7c46977 optimize to only do network calls if necessary 2018-04-22 19:37:24 +02:00
gcarq
bc2bd7fe1e add retrier decorator to all exchange functions except buy/sell 2018-04-22 17:28:49 +02:00
Matthias Voppichler
a70958da41 test modify-logic 2018-04-22 11:05:23 +02:00
Samuel Husso
9f1544978d tests: use only coins that most likely are going to be in bittrex 2018-04-22 11:29:21 +03:00
Matthias Voppichler
f838ba2a9b remove fee column from bot 2018-04-22 10:04:30 +02:00
Samuel Husso
53e76a89ac convert_backtestdata: flake8 fixes 2018-04-22 11:00:51 +03:00
Samuel Husso
de8db9293c exchange: extract ccxt init to its own function (so that we can init ccxt from the scripts) 2018-04-22 10:57:48 +03:00
Samuel Husso
fded8e5117 move download_backtest_data to scripts 2018-04-22 10:56:49 +03:00
Matthias Voppichler
be95d699d2 only update if open_fee is set 2018-04-22 09:13:02 +02:00
gcarq
c43ceb2045 add config*.json to .gitignore 2018-04-22 00:35:04 +02:00
gcarq
9ab4953472 fix backtesting testsuite 2018-04-22 00:21:03 +02:00
gcarq
bbe3bc4423 catch ccxt.ExchangeError and retry 2018-04-22 00:20:15 +02:00
Matthias
acb1b50924 [ccxt] fix unsupported fiat failures (#620)
* prepare to support FIAT/Crypto trading

* Don't fail fiat-convert for unsupported stake currencies

* remove commented code

* Add BNB to cryptomap

* Fix test-failure

* related to random execution as fee was not properly mocked if this is
one of the first tests
2018-04-21 23:20:12 +02:00
Matthias Voppichler
a140748b5a Merge branch 'feat/objectify-ccxt' into cxxt_obj_sellfix 2018-04-21 22:39:22 +02:00
Matthias Voppichler
573b6b8e15 Remove unused line 2018-04-21 22:35:17 +02:00
Matthias
23e989d31f Fix tests run in random order (#599)
* allow tests to run in random mode

* Fix random test mode for fiat-convert

* allow random test execution in persistence

* fix pep8 styling

* use "usefixtures" to prevent pylint "unused parameter" message

* add pytest-random-order to travis
2018-04-21 21:21:50 +02:00
Matthias Voppichler
990f8a996b log in case of error 2018-04-21 21:01:53 +02:00
gcarq
f4077a51c1 log hyperopt progress to stdout instead to the logger 2018-04-21 20:52:01 +02:00
gcarq
403f59ef45 use native python logger 2018-04-21 20:47:06 +02:00
Samuel Husso
001d7443da Merge pull request #618 from gcarq/feature/add-get_fee-mocks
add mocks for exchange.get_fee
2018-04-21 21:26:22 +03:00
Samuel Husso
4eb66aa9ce Merge pull request #617 from gcarq/feature/ccxt-enable-ratelimit
let ccxt handle rate limits internally
2018-04-21 21:25:19 +03:00
Matthias Voppichler
ce90ee4ac2 have backtesting use fee_open and fee_close 2018-04-21 20:05:49 +02:00
Matthias Voppichler
06d230279c Fix tests 2018-04-21 20:05:39 +02:00
Matthias Voppichler
47748bc6f7 adjust tests for fee_open and fee_close 2018-04-21 19:55:48 +02:00
Matthias Voppichler
a620aa8352 add columns fee_open and fee_close, update value 2018-04-21 19:47:08 +02:00
gcarq
09fb4ea584 add mocks for exchange.get_fee 2018-04-21 19:39:18 +02:00
gcarq
3997b6038d let cctx handle rate limits 2018-04-21 19:11:29 +02:00
Matthias Voppichler
7f4c70827a Test get_amount_lots 2018-04-21 13:33:29 +02:00
Matthias Voppichler
f69e8458f4 Add tests for update_real_amount 2018-04-21 13:33:29 +02:00
Matthias Voppichler
02f0f22621 fix comment 2018-04-21 13:33:29 +02:00
Matthias Voppichler
1d43dc229b refactor tests of get_real_amount 2018-04-21 13:33:29 +02:00
Matthias Voppichler
c7d1a767f7 add get_trades_for_order 2018-04-21 13:33:29 +02:00
Matthias Voppichler
11d8f7d522 add get_real_amount and tests 2018-04-21 13:33:29 +02:00
gcarq
1332ab397f fix reference before assignment 2018-04-21 10:19:12 +03:00
Samuel Husso
78bafee39d download_backtest: fix imports and travis 2018-04-19 09:44:45 +03:00
Samuel Husso
66866ff260 fix travis 2018-04-19 09:06:56 +03:00
Samuel Husso
1dcd7e747e partial fix for download testdate 2018-04-19 09:01:34 +03:00
Samuel Husso
42c0d7c7c3 Merge pull request #603 from enenn/ccxt-objectify-pr3_1
[3/3] Add support for multiple exchanges with ccxt (objectified version)
2018-04-18 15:23:33 +03:00
enenn
488210915a Flak8 fixes... 2018-04-15 13:11:17 +02:00
enenn
f1d406b1e6 Fix possible race condition during testing
Order would sometimes fail to sell during tests,
probably because time between current time and creation was 0
2018-04-15 12:50:47 +02:00
enenn
89ed2e0127 Get mocked exhange buy return value from existing fixture 2018-04-15 12:48:02 +02:00
enenn
53b1f8d3a4 Add a 4th pair to testing dynamic whitelist generation 2018-04-15 12:20:49 +02:00
enenn
cc5991d269 Fixturize fee MagicMock object in tests 2018-04-15 12:09:12 +02:00
Michael Egger
b8184e4fdd Merge pull request #602 from xmatthias/obj_ccxt_test_formatms
Add test for format_ms_time
2018-04-13 00:44:25 +02:00
Matthias Voppichler
37dee02e1c Add comment and extract magic number to variable 2018-04-12 19:32:14 +02:00
enenn
2765a065a7 Use UNITTEST/BTC pair instead of ETH/BTC pair for load_data tests 2018-04-12 19:21:40 +02:00
Matthias Voppichler
bb7b2cdfd5 Disable dynamic whitelist
Revert regression introduced in refactoring for objectify

(cherry picked from commit 5bd7954)
2018-04-12 18:35:35 +02:00
enenn
94287d66a8 Flake8 fixes 2018-04-12 18:16:27 +02:00
enenn
1cfa0a3c0e Add exchange name to default hyperopt config 2018-04-12 18:16:26 +02:00
enenn
1678518cd4 Add dry_run=True to config during backtesting 2018-04-12 18:16:26 +02:00
enenn
838bd5824e Mock validate_pairs 2018-04-12 18:16:26 +02:00
enenn
a650072fe0 Edit signal handler tests to work on windows as well 2018-04-12 18:16:26 +02:00
enenn
6115fb08c0 Remove get_fee_maker/taker and add argument to get_fee instead 2018-04-12 18:16:25 +02:00
enenn
91b2092d55 Remove ticker_history_api and ticker_history_without_bv from conftest.py 2018-04-12 18:16:25 +02:00
enenn
cba8745164 Update exchange validate_pairs and related tests 2018-04-12 18:16:19 +02:00
enenn
c3d00a8825 Change ticker format to ccxt in backtesting and optimize tests 2018-04-12 18:14:33 +02:00
enenn
261522446e Change to ccxt ticker format in test_analyze.py 2018-04-12 18:07:45 +02:00
enenn
a86104d0fe Update backtesting and hyperopt tests to use default_config and mock validate_pairs
Use default_config from conftest.py instead of user supplied config in user_data/hyperopt_conf
Mock validate pairs so tests don't fail if pairs don't exist/are removed from exchanges
2018-04-12 18:07:45 +02:00
enenn
4ac2afacfa Use global backtest instance for backtesting tests 2018-04-12 18:07:45 +02:00
enenn
07c655cf41 Use os.path.join for file paths 2018-04-12 18:07:45 +02:00
enenn
a9ba0981c7 Use exchange id for Trade and exchange name for RPC 2018-04-12 18:07:44 +02:00
enenn
7a074f21bd Remove duplicate result pytest fixture 2018-04-12 18:07:44 +02:00
enenn
fef8a4c978 Update tests related to whitelist 2018-04-12 18:07:44 +02:00
enenn
0c8ecf2b1f Add 'get_tickers' function to exchange and use it for dynamic whitelists 2018-04-12 18:07:44 +02:00
enenn
5fc8250ee4 Add 'exchange_has' function to check if exchange supports specific API call
Catch ccxt.NotSupported exception instead of checking beforehand
2018-04-12 18:07:44 +02:00
enenn
e42403fecc Change date to timestamp conversion method in backtesting 2018-04-12 18:07:44 +02:00
enenn
12a84cc30b Mock fee during testing as 0.0025
Ensures profit calculations does not vary if exchange fees change, which can cause tests to fail
2018-04-12 18:07:44 +02:00
enenn
0ae5b75f33 Update order structure to ccxt generic structure instead of bittrex specific 2018-04-12 18:07:43 +02:00
enenn
4810d87044 Change buy/sell return value in tests 2018-04-12 18:07:43 +02:00
enenn
0b71f7186c Replace 'get_wallet_health' and 'get_markets_summaries'
Both are now covered by 'get_markets'
2018-04-12 18:07:43 +02:00
Samuel Husso
eac3c4b72c Merge pull request #600 from enenn/ccxt-obecjtify-pr2_1
[2/3] Add support for multiple exchanges with ccxt (objectified version)
2018-04-12 07:36:18 +03:00
Matthias Voppichler
d03f58417b Fix timezone dependency in test 2018-04-11 20:19:13 +02:00
Matthias Voppichler
7123985325 Add test for format_ms_time 2018-04-10 20:10:20 +02:00
enenn
7eb5138276 Update 8m historical unittest data.
8m.json.gz should be a copy of 1m.json, 8m.json should be empty
2018-04-09 20:25:26 +02:00
enenn
d50445108e Fix issue where datetime string was converted to timestamp with timezone dependent offset 2018-04-08 13:12:55 +02:00
enenn
65c5a0b308 Remove comment from donwload_backtest_data.py 2018-04-08 13:11:36 +02:00
enenn
bfe1eaadcf Adapt convert_backtestdata.py to new format
Also fix timezone issue and integer overflow
2018-04-08 13:11:12 +02:00
enenn
ce3603f84f Change ticker_interval from 5 to 5m in default strategy 2018-04-07 21:31:52 +02:00
enenn
21468d72d3 Fix pair order in test_rpc.py 2018-04-07 20:01:06 +02:00
enenn
4f4cb3698e Revert editing health in conftest.py 2018-04-07 17:05:44 +02:00
enenn
21c5282eb1 Change backtest data from bittrex format to ccxt format 2018-04-07 16:58:26 +02:00
enenn
db46ad6502 Change ticker interval from minutes as integer to string (1m, 5m, 1h,...) 2018-04-07 16:57:47 +02:00
enenn
616006caf8 Replace 'ETH/BTC' with 'UNITTEST/BTC' to fix adx not generating if ETH/BTC ticker history is too short 2018-04-07 16:55:18 +02:00
enenn
cbc0b81d2e Rename ticker history files from "BTC_XXX-1.json" to "XXX_BTC-1m.json" 2018-04-07 16:52:09 +02:00
enenn
c1c6ed6ed7 Replace 'BTC_XXX' with 'XXX/BTC' for pairs and 'XXX_BTC' for files 2018-04-07 16:51:50 +02:00
enenn
1f75636e56 [1/3] Add support for multiple exchanges with ccxt (objectified version) (#585)
* remove obsolete helper functions and make _state a public member.

* remove function assertions

* revert worker() changes

* Update pytest from 3.4.2 to 3.5.0

* Adapt exchange functions to ccxt API
Remove get_market_summaries and get_wallet_health, add exception handling

* Add NetworkException

* Change pair format in constants.py

* Add tests for exchange functions that comply with ccxt

* Remove bittrex tests

* Remove Bittrex and Interface classes

* Add retrier decorator

* Remove cache from get_ticker

* Remove unused and duplicate imports

* Add keyword arguments for get_fee

* Implement 'get_pair_detail_url'

* Change get_ticker_history format to ccxt format

* Fix exchange urls dict, don't need to initialize exchanges

* Add "Using Exchange ..." logging line
2018-04-06 10:57:08 +03:00
Samuel Husso
f3847a3a9a Merge pull request #597 from xmatthias/obj_ccxt_fix_nullref
use local config-object for check_exchange (fixes Nonetype Attribute error when starting the bot)
2018-04-05 08:05:38 +03:00
Matthias Voppichler
0203a48f3e use local config-object for check_exchange
fix AttributeError: 'NoneType' object has no attribute 'get' when
starting the bot.
2018-04-04 22:05:17 +02:00
Michael Egger
5420bb9f6d Merge pull request #594 from xmatthias/obj_ccxt_conv
Conversion script for Ticker history data
2018-03-31 17:58:00 +02:00
Matthias Voppichler
4ac591b076 rename logging to freqtrade 2018-03-31 17:30:11 +02:00
Matthias Voppichler
18f8686cdb fix returncode for convert_file 2018-03-31 17:29:52 +02:00
Matthias Voppichler
2f40e23dcc don't check negated if both trees are handled 2018-03-31 17:28:54 +02:00
Matthias Voppichler
8a83e050d0 use path to handle filenames 2018-03-31 17:24:25 +02:00
Matthias Voppichler
a972b8768d Improve errorhandling for json files which are not ticker data 2018-03-30 23:34:22 +02:00
Matthias Voppichler
a4906c477e Add handling for gzip files 2018-03-30 23:30:23 +02:00
Gerald Lonlas
7cafd1f17e Update exchange unit tests 2018-03-30 13:52:25 -07:00
Gerald Lonlas
3d2c6a22a3 Fix test_validate_pairs() 2018-03-30 13:31:13 -07:00
Gerald Lonlas
052404ffbd Check if the exchange is supported 2018-03-30 13:14:35 -07:00
Gerald Lonlas
96b2210c0f Change deprecated logger.warn by warning 2018-03-30 12:11:06 -07:00
Matthias Voppichler
756bd63e1d whitespace fix 2018-03-26 23:16:41 +02:00
Matthias Voppichler
9d2b7c1fc0 Add convert script 2018-03-26 20:18:14 +02:00
Samuel Husso
0a32d38ad9 exchange: fix get_ticker_history test 2018-03-26 09:24:50 +03:00
Samuel Husso
3069a422e9 Conftest: use coins that we know are in bittrex, added a new conf for ccxt unittest 2018-03-26 09:24:22 +03:00
Samuel Husso
1b4c1980c2 exchange: capitalize class name 2018-03-26 09:23:42 +03:00
Samuel Husso
aba09b8107 Merge pull request #576 from xmatthias/obj-ccxt-ticker
objectify ccxt fix backtesting and some tests
2018-03-26 08:28:40 +03:00
Matthias Voppichler
f51ef1a791 refactor format_ms_time to misc.py 2018-03-25 13:38:50 +02:00
Matthias Voppichler
016232a8e9 Revert OHLVC dataformat to ccxt format
* Also fixes backtesting - but data must be refreshed for now as no
conversation is happening yet
2018-03-25 13:32:46 +02:00
Matthias Voppichler
dbb0a6261f don't raise exceptions from get_ticker_history 2018-03-25 13:03:21 +02:00
Matthias Voppichler
b07ee26e08 Revert testing exchange to bittrex 2018-03-25 12:57:59 +02:00
Matthias Voppichler
ae803474f9 switch rpc_telgram to new style and make it pass 2018-03-24 20:59:25 +01:00
Matthias Voppichler
0a068db285 Switch rpc_test to new currency style 2018-03-24 20:59:09 +01:00
Matthias Voppichler
32222ae6ef Fix tests in acl_pair 2018-03-24 20:42:51 +01:00
Matthias Voppichler
82a2144296 change format of health fixture and get_market_summaries fixture 2018-03-24 20:36:33 +01:00
Matthias Voppichler
22ef860312 Change freqbottest currencies 2018-03-24 20:32:15 +01:00
Matthias Voppichler
a6587b209f freqtradebot_tests - change currency to new format 2018-03-24 20:11:42 +01:00
Matthias Voppichler
4dc1d7538e switch currencies to new format 2018-03-24 20:07:04 +01:00
Matthias Voppichler
609c1eee55 fix persistance tests 2018-03-24 20:03:31 +01:00
Matthias Voppichler
ab6e32f6bb have backtest and dry-mode working
partially revert d20e3f79be - Changing the
OHLVC format should not be done at this time
2018-03-24 19:51:40 +01:00
Matthias Voppichler
85af68d807 ccxt - make backtesting work 2018-03-24 19:45:23 +01:00
Samuel Husso
eb4ac73b78 remove last bittrex references so that bot is runnable 2018-03-22 08:29:52 +02:00
Samuel Husso
d20e3f79be analyze to use the ccxt OHLCV format
setup: remove bittrex and add requirement to ccxt

freqtradebot: update market summaries to ccxt format
2018-03-21 19:57:58 +02:00
Samuel Husso
40a0689183 exhcange now uses ccxt in dry_run, update config 2018-03-21 19:40:16 +02:00
Samuel Husso
14d16d573c Remove bittrex related interface code and tests 2018-03-21 19:31:15 +02:00
Samuel Husso
556533f160 requirements add ccxt, remove bittrex 2018-03-21 19:02:04 +02:00
136 changed files with 6616 additions and 4111 deletions

View File

@@ -2,4 +2,5 @@
omit =
scripts/*
freqtrade/tests/*
freqtrade/vendor/*
freqtrade/vendor/*
freqtrade/__main__.py

View File

@@ -1,15 +1,17 @@
## Step 1: Have you search for this issue before posting it?
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue).
If you have discovered a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue).
If it hasn't been reported, please create a new issue.
## Step 2: Describe your environment
* Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`)
* Branch: Master | Develop
* Last Commit ID: _____ (`git log --format="%H" -n 1`)
## Step 3: Describe the problem:
*Explain the problem you have encountered*
### Steps to reproduce:

View File

@@ -1,5 +1,5 @@
Thank you for sending your pull request. But first, have you included
unit tests, and is your code PEP8 conformant? [More details](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
## Summary
Explain in one sentence the goal of this PR

4
.gitignore vendored
View File

@@ -1,13 +1,14 @@
# Freqtrade rules
freqtrade/tests/testdata/*.json
hyperopt_conf.py
config.json
config*.json
*.sqlite
.hyperopt
logfile.txt
hyperopt_trials.pickle
user_data/
freqtrade-plot.html
freqtrade-profit-plot.html
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -90,3 +91,4 @@ target/
.vscode
.pytest_cache/
.mypy_cache/

4
.pyup.yml Normal file
View File

@@ -0,0 +1,4 @@
# autogenerated pyup.io config file
# see https://pyup.io/docs/configuration/ for all available options
schedule: every day

View File

@@ -13,21 +13,22 @@ addons:
install:
- ./install_ta-lib.sh
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install --upgrade flake8 coveralls pytest-random-order
- pip install --upgrade flake8 coveralls pytest-random-order mypy
- pip install -r requirements.txt
- pip install -e .
jobs:
include:
- script: pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
- script:
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
- coveralls
- script:
- cp config.json.example config.json
- python freqtrade/main.py backtesting
- python freqtrade/main.py --datadir freqtrade/tests/testdata backtesting
- script:
- cp config.json.example config.json
- python freqtrade/main.py hyperopt -e 5
- python freqtrade/main.py --datadir freqtrade/tests/testdata hyperopt -e 5
- script: flake8 freqtrade
after_success:
- coveralls
- script: mypy freqtrade
notifications:
slack:
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=

View File

@@ -7,7 +7,7 @@ Feel like our bot is missing a feature? We welcome your pull requests! Few point
conformant (max-line-length = 100).
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
**Before sending the PR:**
@@ -42,4 +42,21 @@ pip3.6 install flake8 coveralls
flake8 freqtrade
```
We receive a lot of code that fails the `flake8` checks.
To help with that, we encourage you to install the git pre-commit
hook that will warn you when you try to commit code that fails these checks.
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
## 3. Test if all type-hints are correct
**Install packages** (If not already installed)
``` bash
pip3.6 install mypy
```
**Run mypy**
``` bash
mypy freqtrade
```

220
README.md
View File

@@ -1,14 +1,14 @@
# freqtrade
[![Build Status](https://travis-ci.org/gcarq/freqtrade.svg?branch=develop)](https://travis-ci.org/gcarq/freqtrade)
[![Coverage Status](https://coveralls.io/repos/github/gcarq/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/gcarq/freqtrade/maintainability)
[![Build Status](https://travis-ci.org/freqtrade/freqtrade.svg?branch=develop)](https://travis-ci.org/freqtrade/freqtrade)
[![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)
Simple High frequency trading bot for crypto currencies designed to
support multi exchanges and be controlled via Telegram.
![freqtrade](https://raw.githubusercontent.com/gcarq/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
## Disclaimer
This software is for educational purposes only. Do not risk money which
@@ -22,33 +22,10 @@ expect.
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.
## Table of Contents
- [Features](#features)
- [Quick start](#quick-start)
- [Documentations](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
- [Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
- [Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
- [Strategy Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
- [Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
- [Support](#support)
- [Help](#help--slack)
- [Bugs](#bugs--issues)
- [Feature Requests](#feature-requests)
- [Pull Requests](#pull-requests)
- [Basic Usage](#basic-usage)
- [Bot commands](#bot-commands)
- [Telegram RPC commands](#telegram-rpc-commands)
- [Requirements](#requirements)
- [Min hardware required](#min-hardware-required)
- [Software requirements](#software-requirements)
## 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.
## Exchange marketplaces supported
- [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/)
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
## Features
- [x] **Based on Python 3.6+**: For botting on any operating system -
@@ -56,96 +33,68 @@ Windows, macOS and Linux
- [x] **Persistence**: Persistence is achieved through sqlite
- [x] **Dry-run**: Run the bot without playing money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
- [x] **Strategy Optimization**: Optimize your buy/sell strategy
parameters with Hyperopts.
- [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] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell
strategy parameters with real exchange data.
- [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] **Manageable via Telegram**: Manage the bot with Telegram
- [x] **Display profit/loss in fiat**: Display your profit/loss in
33 fiat.
- [x] **Daily summary of profit/loss**: Provide a daily summary
of your profit/loss.
- [x] **Performance status report**: Provide a performance status of
your current trades.
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
- [x] **Performance status report**: Provide a performance status of your current trades.
### Exchange supported
- [x] Bittrex
- [ ] Binance
- [ ] Others
## Table of Contents
- [Quick start](#quick-start)
- [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.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)
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
- [Basic Usage](#basic-usage)
- [Bot commands](#bot-commands)
- [Telegram RPC commands](#telegram-rpc-commands)
- [Support](#support)
- [Help](#help--slack)
- [Bugs](#bugs--issues)
- [Feature Requests](#feature-requests)
- [Pull Requests](#pull-requests)
- [Requirements](#requirements)
- [Min hardware required](#min-hardware-required)
- [Software requirements](#software-requirements)
## Quick start
This quick start section is a very short explanation on how to test the
bot in dry-run. We invite you to read the
[bot documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
to ensure you understand how the bot is working.
### Easy installation
The script below will 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
./setup.sh --install
```
### Manual installation
The following steps are made for Linux/MacOS environment
**1. Clone the repo**
```bash
git clone git@github.com:gcarq/freqtrade.git
git clone git@github.com:freqtrade/freqtrade.git
git checkout develop
cd freqtrade
./setup.sh --install
```
**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
```
_Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_
### Help / Slack
For any questions not covered by the documentation or for further
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).
## Documentation
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)
- [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
- [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
- [Bot usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
- [How to run the bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [How to use Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [How to use Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
- [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
- [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)
### [Bugs / Issues](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
If you discover a bug in the bot, please
[search our issue tracker](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
first. If it hasn't been reported, please
[create a new issue](https://github.com/gcarq/freqtrade/issues/new) and
ensure you follow the template guide so that our team can assist you as
quickly as possible.
### [Feature Requests](https://github.com/gcarq/freqtrade/labels/enhancement)
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/gcarq/freqtrade/labels/enhancement).
If it hasn't been requested, please
[create a new request](https://github.com/gcarq/freqtrade/issues/new)
and ensure you follow the template guide so that it does not get lost
in the bug reports.
### [Pull Requests](https://github.com/gcarq/freqtrade/pulls)
Feel like our bot is missing a feature? We welcome your pull requests!
Please read our
[Contributing document](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
to understand the requirements before sending your pull-requests.
**Important:** Always create your PR against the `develop` branch, not
`master`.
## Basic Usage
### Bot commands
```bash
usage: main.py [-h] [-v] [--version] [-c PATH] [--dry-run-db] [--datadir PATH]
[--dynamic-whitelist [INT]]
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]]
[--dry-run-db]
{backtesting,hyperopt} ...
Simple High Frequency Trading Bot for crypto currencies
@@ -161,23 +110,24 @@ optional arguments:
--version show program's version number and exit
-c PATH, --config PATH
specify configuration file (default: config.json)
--dry-run-db Force dry run to use a local DB
"tradesv3.dry_run.sqlite" instead of memory DB. Work
only if dry_run is enabled.
--datadir PATH path to backtest data (default freqdata/tests/testdata
-d PATH, --datadir PATH
path to backtest data (default:
freqtrade/tests/testdata
-s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path
--dynamic-whitelist [INT]
dynamically generate and update whitelist based on 24h
BaseVolume (Default 20 currencies)
--dry-run-db Force dry run to use a local DB
"tradesv3.dry_run.sqlite" instead of memory DB. Work
only if dry_run is enabled.
```
More details on:
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [How to use Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [How to use Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
### Telegram RPC commands
Telegram is not mandatory. However, this is a great way to control your
bot. More details on our
[documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
[documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md)
- `/start`: Starts the trader
- `/stop`: Stops the trader
@@ -192,6 +142,48 @@ bot. More details on our
- `/help`: Show help message
- `/version`: Show version
## 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.
## Support
### Help / Slack
For any questions not covered by the documentation or for further
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).
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
If you discover a bug in the bot, please
[search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
first. If it hasn't been reported, please
[create a new issue](https://github.com/freqtrade/freqtrade/issues/new) and
ensure you follow the template guide so that our team can assist you as
quickly as possible.
### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement)
Have you a great idea to improve the bot you want to share? Please,
first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement).
If it hasn't been requested, please
[create a new request](https://github.com/freqtrade/freqtrade/issues/new)
and ensure you follow the template guide so that it does not get lost
in the bug reports.
### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls)
Feel like our bot is missing a feature? We welcome your pull requests!
Please read our
[Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md)
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.
**Important:** Always create your PR against the `develop` branch, not
`master`.
## Requirements
### Min hardware required

View File

@@ -3,6 +3,7 @@
"stake_currency": "BTC",
"stake_amount": 0.05,
"fiat_display_currency": "USD",
"ticker_interval" : "5m",
"dry_run": false,
"unfilledtimeout": 600,
"bid_strategy": {
@@ -13,24 +14,25 @@
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"XLM/BTC",
"NXT/BTC",
"POWR/BTC",
"ADA/BTC",
"XMR/BTC"
],
"pair_blacklist": [
"BTC_DOGE"
"DOGE/BTC"
]
},
"experimental": {
"use_sell_signal": false,
"sell_profit_only": false
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
},
"telegram": {
"enabled": true,

View File

@@ -4,7 +4,7 @@
"stake_amount": 0.05,
"fiat_display_currency": "USD",
"dry_run": false,
"ticker_interval": 5,
"ticker_interval": "5m",
"minimal_roi": {
"40": 0.0,
"30": 0.01,
@@ -21,30 +21,32 @@
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"XLM/BTC",
"NXT/BTC",
"POWR/BTC",
"ADA/BTC",
"XMR/BTC"
],
"pair_blacklist": [
"BTC_DOGE"
"DOGE/BTC"
]
},
"experimental": {
"use_sell_signal": false,
"sell_profit_only": false
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
},
"telegram": {
"enabled": true,
"token": "your_telegram_token",
"chat_id": "your_telegram_chat_id"
},
"db_url": "sqlite:///tradesv3.sqlite",
"initial_state": "running",
"internals": {
"process_throttle_secs": 5

View File

@@ -1,137 +1,186 @@
# Backtesting
This page explains how to validate your strategy performance by using
Backtesting.
## Table of Contents
- [Test your strategy with Backtesting](#test-your-strategy-with-backtesting)
- [Understand the backtesting result](#understand-the-backtesting-result)
## Test your strategy with Backtesting
Now you have good Buy and Sell strategies, you want to test it against
real data. This is what we call
[backtesting](https://en.wikipedia.org/wiki/Backtesting).
Backtesting will use the crypto-currencies (pair) from your config file
and load static tickers located in
[/freqtrade/tests/testdata](https://github.com/gcarq/freqtrade/tree/develop/freqtrade/tests/testdata).
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata).
If the 5 min and 1 min ticker for the crypto-currencies to test is not
already in the `testdata` folder, backtesting will download them
automatically. Testdata files will not be updated until you specify it.
The result of backtesting will confirm you if your bot as more chance to
make a profit than a loss.
The result of backtesting will confirm you if your bot has better odds of making a profit than a loss.
The backtesting is very easy with freqtrade.
### Run a backtesting against the currencies listed in your config file
**With 5 min tickers (Per default)**
#### With 5 min tickers (Per default)
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation
```
**With 1 min tickers**
#### With 1 min tickers
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
```
**Reload your testdata files**
#### Update cached pairs with the latest data
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
```
**With live data (do not alter your testdata files)**
#### With live data (do not alter your testdata files)
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --live
```
**Using a different on-disk ticker-data source**
#### Using a different on-disk ticker-data source
```bash
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
```
**With a (custom) strategy file**
```bash
python3 ./freqtrade/main.py -s currentstrategy backtesting
```
Where `-s currentstrategy` refers to a filename `currentstrategy.py` in `freqtrade/user_data/strategies`
#### With a (custom) strategy file
```bash
python3 ./freqtrade/main.py -s TestStrategy backtesting
```
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
#### Exporting trades to file
**Exporting trades to file**
```bash
python3 ./freqtrade/main.py backtesting --export trades
```
**Running backtest with smaller testset**
#### Exporting trades to file specifying a custom filename
```bash
python3 ./freqtrade/main.py backtesting --export trades --export-filename=backtest_teststrategy.json
```
#### Running backtest with smaller testset
Use the `--timerange` argument to change how much of the testset
you want to use. The last N ticks/timeframes will be used.
Example:
```bash
python3 ./freqtrade/main.py backtesting --timerange=-200
```
***Advanced use of timerange***
#### Advanced use of timerange
Doing `--timerange=-200` will get the last 200 timeframes
from your inputdata. You can also specify specific dates,
or a range span indexed by start and stop.
The full timerange specification:
- Use last 123 tickframes of data: `--timerange=-123`
- Use first 123 tickframes of data: `--timerange=123-`
- Use tickframes from line 123 through 456: `--timerange=123-456`
- Use tickframes till 2018/01/31: `--timerange=-20180131`
- Use tickframes since 2018/01/31: `--timerange=20180131-`
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
- Use tickframes between POSIX timestamps 1527595200 1527618600:
`--timerange=1527595200-1527618600`
#### Downloading new set of ticker data
Incoming feature, not implemented yet:
- `--timerange=-20180131`
- `--timerange=20180101-`
- `--timerange=20180101-20181231`
To download new set of backtesting ticker data, you can use a download script.
If you are using Binance for example:
**Update testdata directory**
To update your testdata directory, or download into another testdata directory:
```bash
mkdir -p user_data/data/testdata-20180113
cp freqtrade/tests/testdata/pairs.json user_data/data-20180113
cd user_data/data-20180113
```
Possibly edit pairs.json file to include/exclude pairs
- create a folder `user_data/data/binance` and copy `pairs.json` in that folder.
- update the `pairs.json` to contain the currency pairs you are interested in.
```bash
python3 freqtrade/tests/testdata/download_backtest_data.py -p pairs.json
mkdir -p user_data/data/binance
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
```
The script will read your pairs.json file, and download ticker data
into the current working directory.
Then run:
```bash
python scripts/download_backtest_data --exchange binance
```
For help about backtesting usage, please refer to
[Backtesting commands](#backtesting-commands).
This will download ticker data for all the currency pairs you defined in `pairs.json`.
- To use a different folder than the exchange specific default, use `--export user_data/data/some_directory`.
- To change the exchange used to download the tickers, use `--exchange`. Default is `bittrex`.
- To use `pairs.json` from some other folder, use `--pairs-file some_other_dir/pairs.json`.
- To download ticker data for only 10 days, use `--days 10`.
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
For help about backtesting usage, please refer to [Backtesting commands](#backtesting-commands).
## Understand the backtesting result
The most important in the backtesting is to understand the result.
A backtesting result will look like that:
```
====================== BACKTESTING REPORT ================================
pair buy count avg profit % total profit BTC avg duration
-------- ----------- -------------- ------------------ --------------
BTC_ETH 56 -0.67 -0.00075455 62.3
BTC_LTC 38 -0.48 -0.00036315 57.9
BTC_ETC 42 -1.15 -0.00096469 67.0
BTC_DASH 72 -0.62 -0.00089368 39.9
BTC_ZEC 45 -0.46 -0.00041387 63.2
BTC_XLM 24 -0.88 -0.00041846 47.7
BTC_NXT 24 0.68 0.00031833 40.2
BTC_POWR 35 0.98 0.00064887 45.3
BTC_ADA 43 -0.39 -0.00032292 55.0
BTC_XMR 40 -0.40 -0.00032181 47.4
TOTAL 419 -0.41 -0.00348593 52.9
======================================== BACKTESTING REPORT =========================================
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
| ETH/BTC | 44 | 0.18 | 0.00159118 | 50.9 | 44 | 0 |
| LTC/BTC | 27 | 0.10 | 0.00051931 | 103.1 | 26 | 1 |
| ETC/BTC | 24 | 0.05 | 0.00022434 | 166.0 | 22 | 2 |
| DASH/BTC | 29 | 0.18 | 0.00103223 | 192.2 | 29 | 0 |
| ZEC/BTC | 65 | -0.02 | -0.00020621 | 202.7 | 62 | 3 |
| XLM/BTC | 35 | 0.02 | 0.00012877 | 242.4 | 32 | 3 |
| BCH/BTC | 12 | 0.62 | 0.00149284 | 50.0 | 12 | 0 |
| POWR/BTC | 21 | 0.26 | 0.00108215 | 134.8 | 21 | 0 |
| ADA/BTC | 54 | -0.19 | -0.00205202 | 191.3 | 47 | 7 |
| XMR/BTC | 24 | -0.43 | -0.00206013 | 120.6 | 20 | 4 |
| TOTAL | 335 | 0.03 | 0.00175246 | 157.9 | 315 | 20 |
2018-06-13 06:57:27,347 - freqtrade.optimize.backtesting - INFO -
====================================== LEFT OPEN TRADES REPORT ======================================
| pair | buy count | avg profit % | total profit BTC | avg duration | profit | loss |
|:---------|------------:|---------------:|-------------------:|---------------:|---------:|-------:|
| ETH/BTC | 3 | 0.16 | 0.00009619 | 25.0 | 3 | 0 |
| LTC/BTC | 1 | -1.00 | -0.00020118 | 1085.0 | 0 | 1 |
| ETC/BTC | 2 | -1.80 | -0.00071933 | 1092.5 | 0 | 2 |
| DASH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
| ZEC/BTC | 3 | -4.27 | -0.00256826 | 1301.7 | 0 | 3 |
| XLM/BTC | 3 | -1.11 | -0.00066744 | 965.0 | 0 | 3 |
| BCH/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
| POWR/BTC | 0 | nan | 0.00000000 | nan | 0 | 0 |
| ADA/BTC | 7 | -3.58 | -0.00503604 | 850.0 | 0 | 7 |
| XMR/BTC | 4 | -3.79 | -0.00303456 | 291.2 | 0 | 4 |
| TOTAL | 23 | -2.63 | -0.01213062 | 750.4 | 3 | 20 |
```
The 1st table will contain all trades the bot made.
The 2nd table will contain all trades the bot had to `forcesell` at the end of the backtest period to prsent a full picture.
These trades are also included in the first table, but are extracted separately for clarity.
The last line will give you the overall performance of your strategy,
here:
```
TOTAL 419 -0.41 -0.00348593 52.9
```
@@ -147,6 +196,7 @@ strategy, your sell strategy, and also by the `minimal_roi` and
As for an example if your minimal_roi is only `"0": 0.01`. You cannot
expect the bot to make more profit than 1% (because it will sell every
time a trade will reach 1%).
```json
"minimal_roi": {
"0": 0.01
@@ -159,6 +209,7 @@ profit. Hence, keep in mind that your performance is a mix of your
strategies, your configuration, and the crypto-currency you have set up.
## Next step
Great, your strategy is profitable. What if the bot can give your the
optimal parameters to use for your strategy?
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
Your next step is to learn [how to find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md)

View File

@@ -1,8 +1,10 @@
# Bot Optimization
This page explains where to customize your strategies, and add new
indicators.
This page explains where to customize your strategies, and add new
indicators.
## Table of Contents
- [Install a custom strategy file](#install-a-custom-strategy-file)
- [Customize your strategy](#change-your-strategy)
- [Add more Indicator](#add-more-indicator)
@@ -11,10 +13,12 @@ indicators.
Since the version `0.16.0` the bot allows using custom strategy file.
## Install a custom strategy file
This is very simple. Copy paste your strategy file into the folder
This is very simple. Copy paste your strategy file into the folder
`user_data/strategies`.
Let assume you have a class called `AwesomeStrategy` in the file `awesome-strategy.py`:
1. Move your file into `user_data/strategies` (you should have `user_data/strategies/awesome-strategy.py`
2. Start the bot with the param `--strategy AwesomeStrategy` (the parameter is the class name)
@@ -23,12 +27,14 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy
```
## Change your strategy
The bot includes a default strategy file. However, we recommend you to
The bot includes a default strategy file. However, we recommend you to
use your own file to not have to lose your parameters every time the default
strategy file will be updated on Github. Put your custom strategy file
into the folder `user_data/strategies`.
A strategy file contains all the information needed to build a good strategy:
- Buy strategy rules
- Sell strategy rules
- Minimal ROI recommended
@@ -37,26 +43,29 @@ A strategy file contains all the information needed to build a good strategy:
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`
```bash
``` bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy
```
### Specify custom strategy location
If you want to use a strategy from a different folder you can pass `--strategy-path`
```bash
python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/folder
```
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
file as reference.**
### Buy strategy
Edit the method `populate_buy_trend()` into your strategy file to
### Buy strategy
Edit the method `populate_buy_trend()` into your strategy file to
update your buy strategy.
Sample from `user_data/strategies/test_strategy.py`:
Sample from `user_data/strategies/test_strategy.py`:
```python
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
@@ -76,10 +85,11 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
```
### Sell strategy
Edit the method `populate_sell_trend()` into your strategy file to
update your sell strategy.
Sample from `user_data/strategies/test_strategy.py`:
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
Sample from `user_data/strategies/test_strategy.py`:
```python
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
@@ -98,11 +108,13 @@ def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
```
## Add more Indicator
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.
Sample:
```python
def populate_indicators(dataframe: DataFrame) -> DataFrame:
"""
@@ -137,16 +149,25 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
return dataframe
```
**Want more indicators example?**
Look into the [user_data/strategies/test_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py).
### 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).
Then uncomment indicators you need.
### Where is the default strategy?
The default buy strategy is located in the file
[freqtrade/default_strategy.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
The default buy strategy is located in the file
[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py).
### Further strategy ideas
To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
Feel free to use any of them as inspiration for your own strategies.
We're happy to accept Pull Requests containing new Strategies to that repo.
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) which is a great place to get and/or share ideas.
## Next step
Now you have a perfect strategy you probably want to backtesting it.
Your next step is to learn [How to use the Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md).
Now you have a perfect strategy you probably want to backtest it.
Your next step is to learn [How to use the Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md).

View File

@@ -9,9 +9,10 @@ it.
## Bot commands
```
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
[--dry-run-db]
{backtesting,hyperopt} ...
usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]]
[--db-url PATH]
{backtesting,hyperopt} ...
Simple High Frequency Trading Bot for crypto currencies
@@ -26,17 +27,17 @@ optional arguments:
--version show program's version number and exit
-c PATH, --config PATH
specify configuration file (default: config.json)
-d PATH, --datadir PATH
path to backtest data
-s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path
--dry-run-db Force dry run to use a local DB
"tradesv3.dry_run.sqlite" instead of memory DB. Work
only if dry_run is enabled.
--datadir PATH
path to backtest data (default freqdata/tests/testdata
--dynamic-whitelist [INT]
dynamically generate and update whitelist based on 24h
BaseVolume (Default 20 currencies)
BaseVolume (default: 20)
--db-url PATH Override trades database URL, this is useful if
dry_run is enabled or in custom deployments (default:
sqlite:///tradesv3.sqlite)
```
### How to use a different config file?
@@ -66,7 +67,7 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy
If the bot does not find your strategy file, it will display in an error
message the reason (File not found, or errors in your code).
Learn more about strategy file in [optimize your bot](https://github.com/gcarq/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?
This parameter allows you to add an additional strategy lookup path, which gets
@@ -100,14 +101,14 @@ python3 ./freqtrade/main.py --dynamic-whitelist 30
negative value (e.g -2), `--dynamic-whitelist` will use the default
value (20).
### How to use --dry-run-db?
### How to use --db-url?
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
using `--dry-run-db`. This command will use a separate database file
`tradesv3.dry_run.sqlite`
using `--db-url`. This can also be used to specify a custom database
in production mode. Example command:
```bash
python3 ./freqtrade/main.py -c config.json --dry-run-db
python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite
```
@@ -116,21 +117,32 @@ python3 ./freqtrade/main.py -c config.json --dry-run-db
Backtesting also uses the config specified via `-c/--config`.
```
usage: freqtrade backtesting [-h] [-l] [-i INT] [--realistic-simulation]
[-r]
usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
[--timerange TIMERANGE] [-l] [-r] [--export EXPORT]
[--export-filename EXPORTFILENAME]
optional arguments:
-h, --help show this help message and exit
-l, --live using live data
-i INT, --ticker-interval INT
specify ticker interval in minutes (default: 5)
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
specify ticker interval (1m, 5m, 30m, 1h, 1d)
--realistic-simulation
uses max_open_trades from config to simulate real
world limitations
--timerange TIMERANGE
specify what timerange of data to use.
-l, --live using live data
-r, --refresh-pairs-cached
refresh the pairs files in tests/testdata with
the latest data from Bittrex. Use it if you want
to run your backtesting with up-to-date data.
refresh the pairs files in tests/testdata with the
latest data from the exchange. Use it if you want to
run your backtesting with up-to-date data.
--export EXPORT export backtest results, argument are: trades Example
--export=trades
--export-filename EXPORTFILENAME
Save backtest results to this filename requires
--export to be set as well Example --export-
filename=backtest_today.json (default: backtest-
result.json
```
### How to use --refresh-pairs-cached parameter?
@@ -148,26 +160,33 @@ the parameter `-l` or `--live`.
## Hyperopt commands
It is possible to use hyperopt for trading strategy optimization.
Hyperopt uses an internal json config return by `hyperopt_optimize_conf()`
located in `freqtrade/optimize/hyperopt_conf.py`.
To optimize your strategy, you can use hyperopt parameter hyperoptimization
to find optimal parameter values for your stategy.
```
usage: freqtrade hyperopt [-h] [-e INT] [--use-mongodb]
usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
[--timerange TIMERANGE] [-e INT]
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
optional arguments:
-h, --help show this help message and exit
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
specify ticker interval (1m, 5m, 30m, 1h, 1d)
--realistic-simulation
uses max_open_trades from config to simulate real
world limitations
--timerange TIMERANGE specify what timerange of data to use.
-e INT, --epochs INT specify number of epochs (default: 100)
--use-mongodb parallelize evaluations with mongodb (requires mongod
in PATH)
-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
list. Default: all
```
## A parameter missing in the configuration?
All parameters for `main.py`, `backtesting`, `hyperopt` are referenced
in [misc.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/misc.py#L84)
in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84)
## Next step
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/gcarq/freqtrade/blob/develop/docs/bot-optimization.md).
[optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md).

View File

@@ -16,31 +16,40 @@ The table below will list all configuration parameters.
|----------|---------|----------|-------------|
| `max_open_trades` | 3 | Yes | Number of trades open your bot will have.
| `stake_currency` | BTC | Yes | Crypto-currency used for trading.
| `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.
| `ticker_interval` | [1, 5, 30, 60, 1440] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Defaut is 5 minutes
| `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
| `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.
| `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.
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled 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.
| `exchange.name` | bittrex | Yes | Name of the exchange class to use.
| `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.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_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
| `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.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.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`.
| `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.
| `strategy` | DefaultStrategy | No | Defines Strategy class to use.
| `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.
The definition of each config parameters is in
[misc.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/misc.py#L205).
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
### Understand stake_amount
`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 account an exception is generated.
To allow the bot to trade all the avaliable `stake_currency` in your account set `stake_amount` = `unlimited`.
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
### Understand minimal_roi
`minimal_roi` is a JSON object where the key is a duration
@@ -73,15 +82,35 @@ value. This parameter is optional. If you use it, it will take over the
Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first.
### Understand process_throttle_secs
`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
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.
### Understand ask_last_balance
`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
price. Using `ask` price will guarantee quick success in bid, but bot will also
end up paying more then would probably have been necessary.
### What values for exchange.name?
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
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
with only Bittrex and Binance.
The bot was tested with the following exchanges:
- [Bittrex](https://bittrex.com/): "bittrex"
- [Binance](https://www.binance.com/): "binance"
Feel free to test other exchanges and submit your PR to improve the bot.
### What values for fiat_display_currency?
`fiat_display_currency` set the fiat to use for the conversion form coin to fiat in Telegram.
The valid value 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".
`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".
In addition to central bank currencies, a range of cryto currencies are supported.
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
## Switch to dry-run mode
We recommend starting the bot in dry-run mode to see how your bot will
@@ -91,12 +120,13 @@ creating trades.
### To switch your bot in Dry-run mode:
1. Edit your `config.json` file
2. Switch dry-run to true
2. Switch dry-run to true and specify db_url for a persistent db
```json
"dry_run": true,
"db_url": "sqlite///tradesv3.dryrun.sqlite",
```
3. Remove your Bittrex API key (change them by fake api credentials)
3. Remove your Exchange API key (change them by fake api credentials)
```json
"exchange": {
"name": "bittrex",
@@ -117,24 +147,23 @@ you run it in production mode.
### To switch your bot in production mode:
1. Edit your `config.json` file
2. Switch dry-run to false
2. Switch dry-run to false and don't forget to adapt your database URL if set
```json
"dry_run": false,
```
3. Insert your Bittrex API key (change them by fake api keys)
3. Insert your Exchange API key (change them by fake api keys)
```json
"exchange": {
"name": "bittrex",
"key": "af8ddd35195e9dc500b9a6f799f6f5c93d89193b",
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
...
}
}
```
If you have not your Bittrex API key yet,
[see our tutorial](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md).
[see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
## Next step
Now you have configured your config.json, the next step is to
[start your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md).
[start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).

View File

@@ -27,7 +27,7 @@ like pauses. You can stop your bot, adjust settings and start it again.
#### I want to improve the bot with a new strategy
That's great. We have a nice backtesting and hyperoptimizing setup. See
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands).
the tutorial [here|Testing-new-strategies-with-Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands).
#### Is there a setting to only SELL the coins being held and not
perform anymore BUYS?

View File

@@ -9,18 +9,17 @@ parameters with Hyperopt.
- [Advanced Hyperopt notions](#advanced-notions)
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
- [Execute Hyperopt](#execute-hyperopt)
- [Hyperopt with MongoDB](#hyperopt-with-mongoDB)
- [Understand the hyperopts result](#understand-the-backtesting-result)
## Prepare Hyperopt
Before we start digging in Hyperopt, we recommend you to take a look at
your strategy file located into [user_data/strategies/](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.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
There are two places you need to change in your strategy file to add a
new buy strategy for testing:
- Inside [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294).
- Inside [hyperopt_space()](https://github.com/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
- 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/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`.
There you have two different type of indicators: 1. `guards` and 2.
`triggers`.
@@ -110,13 +109,13 @@ 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/gcarq/freqtrade/blob/develop/user_data/hyperopt_conf.py).
[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/gcarq/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
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.
@@ -194,41 +193,6 @@ Legal values are:
- `stoploss`: search for the best stoploss value
- space-separated list of any of the above values for example `--spaces roi stoploss`
### Hyperopt with MongoDB
Hyperopt with MongoDB, is like Hyperopt under steroids. As you saw by
executing the previous command is the execution takes a long time.
To accelerate it you can use hyperopt with MongoDB.
To run hyperopt with MongoDb you will need 3 terminals.
**Terminal 1: Start MongoDB**
```bash
cd <freqtrade>
source .env/bin/activate
python3 scripts/start-mongodb.py
```
**Terminal 2: Start Hyperopt worker**
```bash
cd <freqtrade>
source .env/bin/activate
python3 scripts/start-hyperopt-worker.py
```
**Terminal 3: Start Hyperopt with MongoDB**
```bash
cd <freqtrade>
source .env/bin/activate
python3 ./freqtrade/main.py -c config.json hyperopt --use-mongodb
```
**Re-run an Hyperopt**
To re-run Hyperopt you have to delete the existing MongoDB table.
```bash
cd <freqtrade>
rm -rf .hyperopt/mongodb/
```
## Understand the hyperopts result
Once Hyperopt is completed you can use the result to adding new buy
signal. Given following result from hyperopt:
@@ -312,4 +276,4 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
## Next step
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/gcarq/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

@@ -6,27 +6,27 @@ Pull-request. Do not hesitate to reach us on
if you do not find the answer to your questions.
## Table of Contents
- [Pre-requisite](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md)
- [Setup your Bittrex account](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
- [Setup your Telegram bot](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
- [Bot Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
- [Install with Docker (all platforms)](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#docker)
- [Install on Linux Ubuntu](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604)
- [Install on MacOS](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#23-macos-installation)
- [Install on Windows](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md#windows)
- [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
- [Bot usage (Start your bot)](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md)
- [Bot commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [Backtesting commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [Hyperopt commands](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
- [Bot Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
- [Change your strategy](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
- [Add more Indicator](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
- [Test your strategy with Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
- [Find optimal parameters with Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
- [Control the bot with telegram](https://github.com/gcarq/freqtrade/blob/develop/docs/telegram-usage.md)
- [Contribute to the project](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
- [How to contribute](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
- [Run tests & Check PEP8 compliance](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
- [FAQ](https://github.com/gcarq/freqtrade/blob/develop/docs/faq.md)
- [SQL cheatsheet](https://github.com/gcarq/freqtrade/blob/develop/docs/sql_cheatsheet.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 Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)
- [Bot Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)
- [Install with Docker (all platforms)](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#docker)
- [Install on Linux Ubuntu](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#21-linux---ubuntu-1604)
- [Install on MacOS](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#23-macos-installation)
- [Install on Windows](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md#windows)
- [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)
- [Bot usage (Start your bot)](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md)
- [Bot commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [Backtesting commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [Hyperopt commands](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
- [Bot Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md)
- [Change your strategy](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#change-your-strategy)
- [Add more Indicator](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md#add-more-indicator)
- [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)
- [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.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)
- [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)
- [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md)

View File

@@ -2,12 +2,13 @@
This page explains how to prepare your environment for running the bot.
To understand how to set up the bot please read the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page.
To understand how to set up the bot please read the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
## Table of Contents
* [Table of Contents](#table-of-contents)
* [Easy Installation - Linux Script](#easy-installation---linux-script)
* [Manual installation](#manual-installation)
* [Automatic Installation - Docker](#automatic-installation---docker)
* [Custom Linux MacOS Installation](#custom-installation)
- [Requirements](#requirements)
@@ -15,7 +16,6 @@ To understand how to set up the bot please read the [Bot Configuration](https://
- [MacOS](#macos)
- [Setup Config and virtual env](#setup-config-and-virtual-env)
* [Windows](#windows)
<!-- /TOC -->
@@ -35,7 +35,9 @@ usage:
```
### --install
This script will install everything you need to run the bot:
* Mandatory software as: `Python3`, `ta-lib`, `wget`
* Setup your virtualenv
* Configure your `config.json` file
@@ -43,14 +45,39 @@ This script will install everything you need to run the bot:
This script is a combination of `install script` `--reset`, `--config`
### --update
Update parameter will pull the last version of your current branch and update your virtualenv.
### --reset
Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv.
### --config
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
@@ -63,13 +90,12 @@ Start by downloading Docker for your platform:
Once you have Docker installed, simply create the config file (e.g. `config.json`) and then create a Docker image for `freqtrade` using the Dockerfile in this repo.
### 1. Prepare the Bot
#### 1.1. Clone the git repository
```bash
git clone https://github.com/gcarq/freqtrade.git
git clone https://github.com/freqtrade/freqtrade.git
```
#### 1.2. (Optional) Checkout the develop branch
@@ -90,21 +116,22 @@ cd freqtrade
cp -n config.json.example config.json
```
> To edit the config please refer to the [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md) page.
> To edit the config please refer to the [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) page.
#### 1.5. Create your database file *(optional - the bot will create it if it is missing)*
Production
```bash
touch tradesv3.sqlite
````
Dry-Run
```bash
touch tradesv3.dryrun.sqlite
```
### 2. Build the Docker image
```bash
@@ -114,7 +141,6 @@ docker build -t freqtrade .
For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates.
### 3. Verify the Docker image
After the build process you can verify that the image was created with:
@@ -123,7 +149,6 @@ After the build process you can verify that the image was created with:
docker images
```
### 4. Run the Docker image
You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory):
@@ -132,8 +157,15 @@ You can run a one-off container that is immediately deleted upon exiting with th
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtime cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd.
```bash
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396)
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.
### 5. Run a restartable docker image
@@ -155,10 +187,11 @@ docker run -d \
-v /etc/localtime:/etc/localtime:ro \
-v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
freqtrade
freqtrade --db-url sqlite:///tradesv3.sqlite
```
If you are using `dry_run=True` it's not necessary to mount `tradesv3.sqlite`, but you can mount `tradesv3.dryrun.sqlite` if you plan to use the dry run mode with the param `--dry-run-db`.
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`
### 6. Monitor your Docker instance
@@ -174,6 +207,26 @@ 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.
### 7. Backtest with docker
The following assumes that the above steps (1-4) have been completed successfully.
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
``` bash
docker run -d \
--name freqtrade \
-v /etc/localtime:/etc/localtime:ro \
-v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
-v ~/.freqtrade/user_data/:/freqtrade/user_data/ \
freqtrade --strategy AwsomelyProfitableStrategy backtesting
```
Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) for more details.
*Note*: Additional parameters can be appended after the image name (`freqtrade` in the above example).
------
## Custom Installation
@@ -183,13 +236,13 @@ We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windo
### Requirements
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
* [pip](https://pip.pypa.io/en/stable/installing/)
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
* [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
### Linux - Ubuntu 16.04
#### 1. Install Python 3.6, Git, and wget
@@ -215,35 +268,20 @@ cd ..
rm -rf ./ta-lib*
```
#### 3. [Optional] Install MongoDB
Install MongoDB if you plan to optimize your strategy with Hyperopt.
```bash
sudo apt-get install mongodb-org
```
> Complete tutorial from Digital Ocean: [How to Install MongoDB on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-16-04).
#### 4. Install FreqTrade
#### 3. Install FreqTrade
Clone the git repository:
```bash
git clone https://github.com/gcarq/freqtrade.git
git clone https://github.com/freqtrade/freqtrade.git
```
Optionally checkout the develop branch:
```bash
git checkout develop
```
#### 5. Configure `freqtrade` as a `systemd` service
#### 4. 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.
After that you can start the daemon with:
```bash
systemctl --user start freqtrade
```
@@ -254,7 +292,6 @@ For this to be persistent (run when user is logged out) you'll need to enable `l
sudo loginctl enable-linger "$USER"
```
### MacOS
#### 1. Install Python 3.6, git, wget and ta-lib
@@ -263,24 +300,12 @@ sudo loginctl enable-linger "$USER"
brew install python3 git wget ta-lib
```
#### 2. [Optional] Install MongoDB
Install MongoDB if you plan to optimize your strategy with Hyperopt.
```bash
curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl-x86_64-3.4.10.tgz
tar -zxvf mongodb-osx-ssl-x86_64-3.4.10.tgz
mkdir -p <path_freqtrade>/env/mongodb
cp -R -n mongodb-osx-x86_64-3.4.10/ <path_freqtrade>/env/mongodb
export PATH=<path_freqtrade>/env/mongodb/bin:$PATH
```
#### 3. Install FreqTrade
#### 2. Install FreqTrade
Clone the git repository:
```bash
git clone https://github.com/gcarq/freqtrade.git
git clone https://github.com/freqtrade/freqtrade.git
```
Optionally checkout the develop branch:
@@ -289,7 +314,6 @@ Optionally checkout the develop branch:
git checkout develop
```
### Setup Config and virtual env
#### 1. Initialize the configuration
@@ -299,8 +323,7 @@ cd freqtrade
cp config.json.example config.json
```
> *To edit the config please refer to [Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md).*
> *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)
@@ -324,27 +347,41 @@ python3.6 ./freqtrade/main.py -c config.json
## Windows
We recommend that Windows users use [Docker](#docker) as this will work
much easier and smoother (also more secure).
We recommend that Windows users use [Docker](#docker) as this will work much easier and smoother (also more secure).
### Install freqtrade
If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work.
If that is not available on your system, feel free to try the instructions below, which led to success for some.
### Install freqtrade manually
#### Clone the git repository
```bash
git clone https://github.com/freqtrade/freqtrade.git
```
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
#### install ta-lib
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of inofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib0.4.17cp36cp36mwin32.whl` (make sure to use the version matching your python version)
```cmd
>cd \path\freqtrade-develop
>python -m venv .env
>cd .env\Scripts
>activate.bat
>cd \path\freqtrade-develop
REM optionally install ta-lib from wheel
REM >pip install TA_Lib0.4.17cp36cp36mwin32.whl
>pip install -r requirements.txt
>pip install -e .
>cd freqtrade
>python main.py
>python freqtrade\main.py
```
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/gcarq/freqtrade/issues/222)
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
Now you have an environment ready, the next step is
[Bot Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)...
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...

View File

@@ -43,6 +43,16 @@ python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200
```
Timerange doesn't work with live data.
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
```
To plot a test strategy the strategy should have first be backtested.
The results may then be plotted with the -s argument:
```
python scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data/<exchange_name>/
```
## Plot profit

View File

@@ -32,9 +32,12 @@ CREATE TABLE trades (
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL,
fee_open FLOAT NOT NULL,
fee_close FLOAT NOT NULL,
open_rate FLOAT,
open_rate_requested FLOAT,
close_rate FLOAT,
close_rate_requested FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
@@ -71,20 +74,20 @@ WHERE id=31;
```sql
INSERT
INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
```
**Example:**
```sql
INSERT INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000')
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000')
```
## Fix wrong fees in the table
If your DB was created before
[PR#200](https://github.com/gcarq/freqtrade/pull/200) was merged
[PR#200](https://github.com/freqtrade/freqtrade/pull/200) was merged
(before 12/23/17).
```sql
UPDATE trades SET fee=0.0025 WHERE fee=0.005;
```
```

View File

@@ -4,7 +4,7 @@ This page explains how to command your bot with Telegram.
## Pre-requisite
To control your bot with Telegram, you need first to
[set up a Telegram bot](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md)
[set up a Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
and add your Telegram API keys into your config file.
## Telegram commands
@@ -16,6 +16,7 @@ official commands. You can ask at any moment for help with `/help`.
|----------|---------|-------------|
| `/start` | | Starts the trader
| `/stop` | | Stops the trader
| `/reload_conf` | | Reloads the configuration file
| `/status` | | Lists all open trades
| `/status table` | | List all open trades in a table format
| `/count` | | Displays number of trades used and available
@@ -42,7 +43,7 @@ Below, example of Telegram message you will receive for each command.
For each open trade, the bot will send you the following message.
> **Trade ID:** `123`
> **Current Pair:** BTC_CVC
> **Current Pair:** CVC/BTC
> **Open Since:** `1 days ago`
> **Amount:** `26.64180098`
> **Open Rate:** `0.00007489`
@@ -57,8 +58,8 @@ Return the status of all open trades in a table format.
```
ID Pair Since Profit
---- -------- ------- --------
67 BTC_SC 1 d 13.33%
123 BTC_CVC 1 h 12.95%
67 SC/BTC 1 d 13.33%
123 CVC/BTC 1 h 12.95%
```
## /count
@@ -83,7 +84,7 @@ Return a summary of your profit/loss and performance.
> **First Trade opened:** `3 days ago`
> **Latest Trade opened:** `2 minutes ago`
> **Avg. Duration:** `2:33:45`
> **Best Performing:** `BTC_PAY: 50.23%`
> **Best Performing:** `PAY/BTC: 50.23%`
## /forcesell <trade_id>
@@ -92,11 +93,11 @@ Return a summary of your profit/loss and performance.
## /performance
Return the performance of each crypto-currency the bot has sold.
> Performance:
> 1. `BTC_RCN 57.77%`
> 2. `BTC_PAY 56.91%`
> 3. `BTC_VIB 47.07%`
> 4. `BTC_SALT 30.24%`
> 5. `BTC_STORJ 27.24%`
> 1. `RCN/BTC 57.77%`
> 2. `PAY/BTC 56.91%`
> 3. `VIB/BTC 47.07%`
> 4. `SALT/BTC 30.24%`
> 5. `STORJ/BTC 27.24%`
> ...
## /balance
@@ -129,12 +130,8 @@ Day Profit BTC Profit USD
> **Version:** `0.14.3`
### using proxy with telegram
in [freqtrade/freqtrade/rpc/telegram.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/rpc/telegram.py) replace
```
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
```
with
```
self._updater = Updater(token=self._config['telegram']['token'], request_kwargs={'proxy_url': 'socks5://127.0.0.1:1080/'}, workers=0)
$ export HTTP_PROXY="http://addr:port"
$ export HTTPS_PROXY="http://addr:port"
$ freqtrade
```

View File

@@ -1,5 +1,5 @@
""" FreqTrade bot """
__version__ = '0.16.1'
__version__ = '0.17.0'
class DependencyException(BaseException):
@@ -12,5 +12,14 @@ class DependencyException(BaseException):
class OperationalException(BaseException):
"""
Requires manual intervention.
This happens when an exchange returns an unexpected error during runtime.
This happens when an exchange returns an unexpected error during runtime
or given configuration is invalid.
"""
class TemporaryError(BaseException):
"""
Temporary network or exchange related error.
This could happen when an exchange is congested, unavailable, or the user
has networking problems. Usually resolves itself after a time.
"""

15
freqtrade/__main__.py Normal file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python3
"""
__main__.py for Freqtrade
To launch Freqtrade as a module
> python -m freqtrade (with Python >= 3.6)
"""
import sys
from freqtrade import main
if __name__ == '__main__':
main.set_loggers()
main.main(sys.argv[1:])

View File

@@ -9,9 +9,10 @@ from typing import Dict, List, Tuple
import arrow
from pandas import DataFrame, to_datetime
from freqtrade.exchange import get_ticker_history
from freqtrade import constants
from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import StrategyResolver
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
logger = logging.getLogger(__name__)
@@ -36,7 +37,7 @@ class Analyze(object):
:param config: Bot configuration (use the one from Configuration())
"""
self.config = config
self.strategy = StrategyResolver(self.config).strategy
self.strategy: IStrategy = StrategyResolver(self.config).strategy
@staticmethod
def parse_ticker_dataframe(ticker: list) -> DataFrame:
@@ -45,21 +46,23 @@ class Analyze(object):
:param ticker: See exchange.get_ticker_history
:return: DataFrame
"""
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
frame = DataFrame(ticker).rename(columns=columns)
if 'BV' in frame:
frame.drop('BV', axis=1, inplace=True)
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
frame = DataFrame(ticker, columns=cols)
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
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({
'close': 'last',
'open': 'first',
'high': 'max',
'low': 'min',
'open': 'first',
'close': 'last',
'volume': 'max',
})
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
return frame
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
@@ -88,13 +91,20 @@ class Analyze(object):
"""
return self.strategy.populate_sell_trend(dataframe=dataframe)
def get_ticker_interval(self) -> int:
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
@@ -107,14 +117,14 @@ class Analyze(object):
dataframe = self.populate_sell_trend(dataframe)
return dataframe
def get_signal(self, pair: str, interval: int) -> Tuple[bool, bool]:
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 BTC_ANT or BTC-ANT
: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 = get_ticker_history(pair, interval)
ticker_hist = exchange.get_ticker_history(pair, interval)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return False, False
@@ -144,7 +154,8 @@ class Analyze(object):
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
if signal_date < arrow.utcnow() - timedelta(minutes=(interval + 5)):
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,
@@ -168,33 +179,45 @@ class Analyze(object):
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_rate=rate, current_time=date):
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
logger.debug('Required profit reached. Selling..')
return True
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
if self.config.get('experimental', {}).get('sell_profit_only', False):
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 self.config.get('experimental', {}).get('use_sell_signal', False):
if sell and not buy and experimental.get('use_sell_signal', False):
logger.debug('Sell signal received. Selling..')
return True
return False
def min_roi_reached(self, trade: Trade, current_rate: float, current_time: datetime) -> bool:
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
"""
current_profit = trade.calc_profit_percent(current_rate)
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
logger.debug('Stop loss hit.')
return True
# Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60

View File

@@ -2,23 +2,36 @@
This module contains the argument manager class
"""
import os
import argparse
import logging
import os
import re
from typing import List, Tuple, Optional
import arrow
from typing import List, Optional, NamedTuple
from freqtrade import __version__, constants
class TimeRange(NamedTuple):
"""
NamedTuple Defining timerange inputs.
[start/stop]type defines if [start/stop]ts shall be used.
if *type is none, don't use corresponding startvalue.
"""
starttype: Optional[str] = None
stoptype: Optional[str] = None
startts: int = 0
stopts: int = 0
class Arguments(object):
"""
Arguments Class. Manage the arguments received by the cli
"""
def __init__(self, args: List[str], description: str):
def __init__(self, args: List[str], description: str) -> None:
self.args = args
self.parsed_arg = None
self.parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description=description)
def _load_args(self) -> None:
@@ -59,7 +72,7 @@ class Arguments(object):
self.parser.add_argument(
'--version',
action='version',
version='%(prog)s {}'.format(__version__),
version=f'%(prog)s {__version__}'
)
self.parser.add_argument(
'-c', '--config',
@@ -71,9 +84,9 @@ class Arguments(object):
)
self.parser.add_argument(
'-d', '--datadir',
help='path to backtest data (default: %(default)s',
help='path to backtest data',
dest='datadir',
default=os.path.join('freqtrade', 'tests', 'testdata'),
default=None,
type=str,
metavar='PATH',
)
@@ -94,8 +107,8 @@ class Arguments(object):
)
self.parser.add_argument(
'--dynamic-whitelist',
help='dynamically generate and update whitelist \
based on 24h BaseVolume (Default 20 currencies)', # noqa
help='dynamically generate and update whitelist'
' based on 24h BaseVolume (default: %(const)s)',
dest='dynamic_whitelist',
const=constants.DYNAMIC_WHITELIST,
type=int,
@@ -103,11 +116,13 @@ class Arguments(object):
nargs='?',
)
self.parser.add_argument(
'--dry-run-db',
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
instead of memory DB. Work only if dry_run is enabled.',
action='store_true',
dest='dry_run_db',
'--db-url',
help='Override trades database URL, this is useful if dry_run is enabled'
' or in custom deployments (default: %(default)s)',
dest='db_url',
default=constants.DEFAULT_DB_PROD_URL,
type=str,
metavar='PATH',
)
@staticmethod
@@ -123,8 +138,8 @@ class Arguments(object):
)
parser.add_argument(
'-r', '--refresh-pairs-cached',
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
Use it if you want to run your backtesting with up-to-date data.',
help='refresh the pairs files in tests/testdata with the latest data from the '
'exchange. Use it if you want to run your backtesting with up-to-date data.',
action='store_true',
dest='refresh_pairs',
)
@@ -136,15 +151,30 @@ class Arguments(object):
default=None,
dest='export',
)
parser.add_argument(
'--export-filename',
help='Save backtest results to this filename \
requires --export to be set as well\
Example --export-filename=user_data/backtest_data/backtest_today.json\
(default: %(default)s)',
type=str,
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
dest='exportfilename',
metavar='PATH',
)
@staticmethod
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
"""
Parses given common arguments for Backtesting and Hyperopt scripts.
:param parser:
:return:
"""
parser.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval in minutes (1, 5, 30, 60, 1440)',
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
dest='ticker_interval',
type=int,
metavar='INT',
type=str,
)
parser.add_argument(
'--realistic-simulation',
@@ -173,12 +203,6 @@ class Arguments(object):
type=int,
metavar='INT',
)
parser.add_argument(
'--use-mongodb',
help='parallelize evaluations with mongodb (requires mongod in PATH)',
dest='mongodb',
action='store_true',
)
parser.add_argument(
'-s', '--spaces',
help='Specify which parameters to hyperopt. Space separate list. \
@@ -211,17 +235,20 @@ class Arguments(object):
self.hyperopt_options(hyperopt_cmd)
@staticmethod
def parse_timerange(text: str) -> Optional[Tuple[List, int, int]]:
def parse_timerange(text: Optional[str]) -> TimeRange:
"""
Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange
:return: Start and End range period
"""
if text is None:
return None
return TimeRange(None, None, 0, 0)
syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
@@ -231,23 +258,27 @@ class Arguments(object):
if match: # Regex has matched
rvals = match.groups()
index = 0
start = None
stop = None
start: int = 0
stop: int = 0
if stype[0]:
start = rvals[index]
if stype[0] != 'date':
start = int(start)
starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8:
start = arrow.get(starts, 'YYYYMMDD').timestamp
else:
start = int(starts)
index += 1
if stype[1]:
stop = rvals[index]
if stype[1] != 'date':
stop = int(stop)
return stype, start, stop
stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8:
stop = arrow.get(stops, 'YYYYMMDD').timestamp
else:
stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop)
raise Exception('Incorrect syntax for timerange "%s"' % text)
def scripts_options(self) -> None:
"""
Parses given arguments for plot scripts.
Parses given arguments for scripts.
"""
self.parser.add_argument(
'-p', '--pair',
@@ -255,3 +286,51 @@ class Arguments(object):
dest='pair',
default=None
)
def testdata_dl_options(self) -> None:
"""
Parses given arguments for testdata download
"""
self.parser.add_argument(
'--pairs-file',
help='File containing a list of pairs to download',
dest='pairs_file',
default=None,
metavar='PATH',
)
self.parser.add_argument(
'--export',
help='Export files to given dir',
dest='export',
default=None,
metavar='PATH',
)
self.parser.add_argument(
'--days',
help='Download data for number of days',
dest='days',
type=int,
metavar='INT',
default=None
)
self.parser.add_argument(
'--exchange',
help='Exchange name (default: %(default)s)',
dest='exchange',
type=str,
default='bittrex'
)
self.parser.add_argument(
'-t', '--timeframes',
help='Specify which tickers to download. Space separated list. \
Default: %(default)s',
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
'6h', '8h', '12h', '1d', '3d', '1w'],
default=['1m', '5m'],
nargs='+',
dest='timeframes',
)

View File

@@ -1,16 +1,16 @@
"""
This module contains the configuration class
"""
import os
import json
import logging
from argparse import Namespace
from typing import Dict, Any
from typing import Optional, Dict, Any
from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match
import ccxt
from freqtrade import constants
from freqtrade import OperationalException, constants
logger = logging.getLogger(__name__)
@@ -23,7 +23,7 @@ class Configuration(object):
"""
def __init__(self, args: Namespace) -> None:
self.args = args
self.config = None
self.config: Optional[Dict[str, Any]] = None
def load_config(self) -> Dict[str, Any]:
"""
@@ -61,11 +61,9 @@ class Configuration(object):
with open(path) as file:
conf = json.load(file)
except FileNotFoundError:
logger.critical(
'Config file "%s" not found. Please create your config file',
path
)
exit(0)
raise OperationalException(
'Config file "{}" not found!'
' Please create a config file or check whether it exists.'.format(path))
if 'internals' not in conf:
conf['internals'] = {}
@@ -97,19 +95,35 @@ class Configuration(object):
'(not applicable with Backtesting and Hyperopt)'
)
# Add dry_run_db if found and the bot in dry run
if self.args.dry_run_db and config.get('dry_run', False):
config.update({'dry_run_db': True})
logger.info('Parameter --dry-run-db detected ...')
if self.args.db_url != constants.DEFAULT_DB_PROD_URL:
config.update({'db_url': self.args.db_url})
logger.info('Parameter --db-url detected ...')
if config.get('dry_run_db', False):
if config.get('dry_run', False):
logger.info('Dry_run will use the DB file: "tradesv3.dry_run.sqlite"')
else:
logger.info('Dry run is disabled. (--dry_run_db ignored)')
if config.get('dry_run', False):
logger.info('Dry run is enabled')
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
# Default to in-memory db for dry_run if not specified
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
else:
if not config.get('db_url', None):
config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled')
logger.info('Using DB: "{}"'.format(config['db_url']))
# Check if the exchange set by the user is supported
self.check_exchange(config)
return config
def _create_default_datadir(self, config: Dict[str, Any]) -> str:
exchange_name = config.get('exchange', {}).get('name').lower()
default_path = os.path.join('user_data', 'data', exchange_name)
if not os.path.isdir(default_path):
os.makedirs(default_path)
logger.info(f'Created data directory: {default_path}')
return default_path
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load Backtesting configuration
@@ -121,7 +135,7 @@ class Configuration(object):
if 'ticker_interval' in self.args and self.args.ticker_interval:
config.update({'ticker_interval': self.args.ticker_interval})
logger.info('Parameter -i/--ticker-interval detected ...')
logger.info('Using ticker_interval: %d ...', config.get('ticker_interval'))
logger.info('Using ticker_interval: %s ...', config.get('ticker_interval'))
# If -l/--live is used we add it to the configuration
if 'live' in self.args and self.args.live:
@@ -142,7 +156,9 @@ class Configuration(object):
# If --datadir is used we add it to the configuration
if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': self.args.datadir})
logger.info('Parameter --datadir detected: %s ...', self.args.datadir)
else:
config.update({'datadir': self._create_default_datadir(config)})
logger.info('Using data folder: %s ...', config.get('datadir'))
# If -r/--refresh-pairs-cached is used we add it to the configuration
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
@@ -154,6 +170,11 @@ class Configuration(object):
config.update({'export': self.args.export})
logger.info('Parameter --export detected: %s ...', self.args.export)
# If --export-filename is used we add it to the configuration
if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename:
config.update({'exportfilename': self.args.exportfilename})
logger.info('Storing backtest results to %s ...', self.args.exportfilename)
return config
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
@@ -167,11 +188,6 @@ class Configuration(object):
logger.info('Parameter --epochs detected ...')
logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
# If --mongodb is used we add it to the configuration
if 'mongodb' in self.args and self.args.mongodb:
config.update({'mongodb': self.args.mongodb})
logger.info('Parameter --use-mongodb detected ...')
# If --spaces is used we add it to the configuration
if 'spaces' in self.args and self.args.spaces:
config.update({'spaces': self.args.spaces})
@@ -189,7 +205,7 @@ class Configuration(object):
validate(conf, constants.CONF_SCHEMA)
return conf
except ValidationError as exception:
logger.fatal(
logger.critical(
'Invalid configuration. See config.json.example. Reason: %s',
exception
)
@@ -206,3 +222,22 @@ class Configuration(object):
self.config = self.load_config()
return self.config
def check_exchange(self, config: Dict[str, Any]) -> bool:
"""
Check if the exchange name in the config file is supported by Freqtrade
:return: True or raised an exception if the exchange if not supported
"""
exchange = config.get('exchange', {}).get('name').lower()
if exchange not in ccxt.exchanges:
exception_msg = f'Exchange "{exchange}" not supported.\n' \
f'The following exchanges are supported: {", ".join(ccxt.exchanges)}'
logger.critical(exception_msg)
raise OperationalException(
exception_msg
)
logger.debug('Exchange "%s" supported', exchange)
return True

View File

@@ -9,23 +9,49 @@ TICKER_INTERVAL = 5 # min
HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
UNLIMITED_STAKE_AMOUNT = 'unlimited'
TICKER_INTERVAL_MINUTES = {
'1m': 1,
'3m': 3,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'2h': 120,
'4h': 240,
'6h': 360,
'8h': 480,
'12h': 720,
'1d': 1440,
'3d': 4320,
'1w': 10080,
}
SUPPORTED_FIAT = [
"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",
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
]
# Required json-schema for user specified config
CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 1},
'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
'stake_amount': {'type': 'number', 'minimum': 0.0005},
'fiat_display_currency': {'type': 'string', 'enum': ['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']},
'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': {
"type": ["number", "string"],
"minimum": 0.0005,
"pattern": UNLIMITED_STAKE_AMOUNT
},
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
@@ -53,7 +79,8 @@ CONF_SCHEMA = {
'type': 'object',
'properties': {
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}
'sell_profit_only': {'type': 'boolean'},
"ignore_roi_if_buy_signal_true": {'type': 'boolean'}
}
},
'telegram': {
@@ -65,6 +92,7 @@ CONF_SCHEMA = {
},
'required': ['enabled', 'token', 'chat_id']
},
'db_url': {'type': 'string'},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': {
'type': 'object',
@@ -85,7 +113,7 @@ CONF_SCHEMA = {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
},
'uniqueItems': True
},
@@ -93,7 +121,7 @@ CONF_SCHEMA = {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
},
'uniqueItems': True
}

View File

@@ -1,185 +1,417 @@
# pragma pylint: disable=W0603
""" Cryptocurrency Exchanges support """
import enum
import logging
from random import randint
from typing import List, Dict, Any, Optional
from datetime import datetime
import ccxt
import arrow
import requests
from cachetools import cached, TTLCache
from freqtrade import OperationalException
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.interface import Exchange
from freqtrade import constants, OperationalException, DependencyException, TemporaryError
logger = logging.getLogger(__name__)
# Current selected exchange
_API: Exchange = None
_CONF: dict = {}
# Holds all open sell orders for dry_run
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
API_RETRY_COUNT = 4
class Exchanges(enum.Enum):
"""
Maps supported exchange names to correspondent classes.
"""
BITTREX = Bittrex
# Urls to exchange markets, insert quote and base with .format()
_EXCHANGE_URLS = {
ccxt.bittrex.__name__: '/Market/Index?MarketName={quote}-{base}',
ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}'
}
def init(config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
global _CONF, _API
_CONF.update(config)
if config['dry_run']:
logger.info('Instance is running with dry_run enabled')
exchange_config = config['exchange']
# Find matching class for the given exchange name
name = exchange_config['name']
try:
exchange_class = Exchanges[name.upper()].value
except KeyError:
raise OperationalException('Exchange {} is not supported'.format(name))
_API = exchange_class(exchange_config)
# Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist'])
def retrier(f):
def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT)
try:
return 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 wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper
def validate_pairs(pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
Raises OperationalException if one pair is not available.
:param pairs: list of pairs
:return: None
"""
try:
markets = _API.get_markets()
except requests.exceptions.RequestException as e:
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return
class Exchange(object):
stake_cur = _CONF['stake_currency']
for pair in pairs:
if not pair.startswith(stake_cur):
# Current selected exchange
_api: ccxt.Exchange = None
_conf: Dict = {}
_cached_ticker: Dict[str, Any] = {}
# Holds all open sell orders for dry_run
_dry_run_open_orders: Dict[str, Any] = {}
def __init__(self, config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:return: None
"""
self._conf.update(config)
if config['dry_run']:
logger.info('Instance is running with dry_run enabled')
exchange_config = config['exchange']
self._api = self._init_ccxt(exchange_config)
logger.info('Using Exchange "%s"', self.name)
# Check if all pairs are available
self.validate_pairs(config['exchange']['pair_whitelist'])
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange:
"""
Initialize ccxt with given config and return valid
ccxt instance.
"""
# Find matching class for the given exchange name
name = exchange_config['name']
if name not in ccxt.exchanges:
raise OperationalException(f'Exchange {name} is not supported')
try:
api = getattr(ccxt, name.lower())({
'apiKey': exchange_config.get('key'),
'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''),
'enableRateLimit': True,
})
except (KeyError, AttributeError):
raise OperationalException(f'Exchange {name} is not supported')
return api
@property
def name(self) -> str:
"""exchange Name (from ccxt)"""
return self._api.name
@property
def id(self) -> str:
"""exchange ccxt id"""
return self._api.id
def validate_pairs(self, pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
Raises OperationalException if one pair is not available.
:param pairs: list of pairs
:return: None
"""
try:
markets = self._api.load_markets()
except ccxt.BaseError as e:
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return
stake_cur = self._conf['stake_currency']
for pair in pairs:
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
# TODO: add a support for having coins in BTC/USDT format
if not pair.endswith(stake_cur):
raise OperationalException(
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
if pair not in markets:
raise OperationalException(
f'Pair {pair} is not available at {self.name}')
def exchange_has(self, endpoint: str) -> bool:
"""
Checks if exchange implements a specific API endpoint.
Wrapper around ccxt 'has' attribute
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
:return: bool
"""
return endpoint in self._api.has and self._api.has[endpoint]
def buy(self, pair: str, rate: float, amount: float) -> Dict:
if self._conf['dry_run']:
order_id = f'dry_run_buy_{randint(0, 10**6)}'
self._dry_run_open_orders[order_id] = {
'pair': pair,
'price': rate,
'amount': amount,
'type': 'limit',
'side': 'buy',
'remaining': 0.0,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed',
'fee': None
}
return {'id': order_id}
try:
return self._api.create_limit_buy_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
f'Insufficient funds to create limit buy order on market {pair}.'
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not create limit buy order on market {pair}.'
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def sell(self, pair: str, rate: float, amount: float) -> Dict:
if self._conf['dry_run']:
order_id = f'dry_run_sell_{randint(0, 10**6)}'
self._dry_run_open_orders[order_id] = {
'pair': pair,
'price': rate,
'amount': amount,
'type': 'limit',
'side': 'sell',
'remaining': 0.0,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed'
}
return {'id': order_id}
try:
return self._api.create_limit_sell_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
f'Insufficient funds to create limit sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not create limit sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_balance(self, currency: str) -> float:
if self._conf['dry_run']:
return 999.9
# ccxt exception is already handled by get_balances
balances = self.get_balances()
balance = balances.get(currency)
if balance is None:
raise TemporaryError(
f'Could not get {currency} balance due to malformed exchange response: {balances}')
return balance['free']
@retrier
def get_balances(self) -> dict:
if self._conf['dry_run']:
return {}
try:
balances = self._api.fetch_balance()
# Remove additional info from ccxt results
balances.pop("info", None)
balances.pop("free", None)
balances.pop("total", None)
balances.pop("used", None)
return balances
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_tickers(self) -> Dict:
try:
return self._api.fetch_tickers()
except ccxt.NotSupported as e:
raise OperationalException(
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
)
if pair not in markets:
f'Exchange {self._api.name} does not support fetching tickers in batch.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self._cached_ticker.keys():
try:
data = self._api.fetch_ticker(pair)
try:
self._cached_ticker[pair] = {
'bid': float(data['bid']),
'ask': float(data['ask']),
}
except KeyError:
logger.debug("Could not cache ticker data for %s", pair)
return data
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(e)
else:
logger.info("returning cached ticker-data for %s", pair)
return self._cached_ticker[pair]
@retrier
def get_ticker_history(self, pair: str, tick_interval: str,
since_ms: Optional[int] = None) -> List[Dict]:
try:
# last item should be in the time interval [now - tick_interval, now]
till_time_ms = arrow.utcnow().shift(
minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval]
).timestamp * 1000
# it looks as if some exchanges return cached data
# and they update it one in several minute, so 10 mins interval
# is necessary to skeep downloading of an empty array when all
# chached data was already downloaded
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
data: List[Dict[Any, Any]] = []
while not since_ms or since_ms < till_time_ms:
data_part = self._api.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_part = sorted(data_part, key=lambda x: x[0])
if not data_part:
break
logger.debug('Downloaded data for %s time range [%s, %s]',
pair,
arrow.get(data_part[0][0] / 1000).format(),
arrow.get(data_part[-1][0] / 1000).format())
data.extend(data_part)
since_ms = data[-1][0] + 1
return data
except ccxt.NotSupported as e:
raise OperationalException(
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
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
def cancel_order(self, order_id: str, pair: str) -> None:
if self._conf['dry_run']:
return
def buy(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
'amount': amount,
'type': 'LIMIT_BUY',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
}
return order_id
try:
return self._api.cancel_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not cancel order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
return _API.buy(pair, rate, amount)
@retrier
def get_order(self, order_id: str, pair: str) -> Dict:
if self._conf['dry_run']:
order = self._dry_run_open_orders[order_id]
order.update({
'id': order_id
})
return order
try:
return self._api.fetch_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
f'Could not get order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
if self._conf['dry_run']:
return []
if not self.exchange_has('fetchMyTrades'):
return []
try:
my_trades = self._api.fetch_my_trades(pair, since.timestamp())
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
def sell(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
'amount': amount,
'type': 'LIMIT_SELL',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
}
return order_id
return matched_trades
return _API.sell(pair, rate, amount)
except ccxt.NetworkError as e:
raise TemporaryError(
f'Could not get trades due to networking error. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
def get_pair_detail_url(self, pair: str) -> str:
try:
url_base = self._api.urls.get('www')
base, quote = pair.split('/')
def get_balance(currency: str) -> float:
if _CONF['dry_run']:
return 999.9
return url_base + _EXCHANGE_URLS[self._api.id].format(base=base, quote=quote)
except KeyError:
logger.warning('Could not get exchange url for %s', self.name)
return ""
return _API.get_balance(currency)
@retrier
def get_markets(self) -> List[dict]:
try:
return self._api.fetch_markets()
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load markets due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1,
price=1, taker_or_maker='maker') -> float:
try:
# validate that markets are loaded before trying to get fee
if self._api.markets is None or len(self._api.markets) == 0:
self._api.load_markets()
def get_balances():
if _CONF['dry_run']:
return []
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
price=price, takerOrMaker=taker_or_maker)['rate']
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
return _API.get_balances()
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
return _API.get_ticker(pair, refresh)
@cached(TTLCache(maxsize=100, ttl=30))
def get_ticker_history(pair: str, tick_interval) -> List[Dict]:
return _API.get_ticker_history(pair, tick_interval)
def cancel_order(order_id: str) -> None:
if _CONF['dry_run']:
return
return _API.cancel_order(order_id)
def get_order(order_id: str) -> Dict:
if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id]
order.update({
'id': order_id
})
return order
return _API.get_order(order_id)
def get_pair_detail_url(pair: str) -> str:
return _API.get_pair_detail_url(pair)
def get_markets() -> List[str]:
return _API.get_markets()
def get_market_summaries() -> List[Dict]:
return _API.get_market_summaries()
def get_name() -> str:
return _API.name
def get_fee() -> float:
return _API.fee
def get_wallet_health() -> List[Dict]:
return _API.get_wallet_health()
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

@@ -1,211 +0,0 @@
import logging
from typing import Dict, List, Optional
from bittrex.bittrex import API_V1_1, API_V2_0
from bittrex.bittrex import Bittrex as _Bittrex
from requests.exceptions import ContentDecodingError
from freqtrade import OperationalException
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
_API: _Bittrex = None
_API_V2: _Bittrex = None
_EXCHANGE_CONF: dict = {}
class Bittrex(Exchange):
"""
Bittrex API wrapper.
"""
# Base URL and API endpoints
BASE_URL: str = 'https://www.bittrex.com'
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
def __init__(self, config: dict) -> None:
global _API, _API_V2, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config)
_API = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V1_1,
)
_API_V2 = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V2_0,
)
self.cached_ticker = {}
@staticmethod
def _validate_response(response) -> None:
"""
Validates the given bittrex response
and raises a ContentDecodingError if a non-fatal issue happened.
"""
temp_error_messages = [
'NO_API_RESPONSE',
'MIN_TRADE_REQUIREMENT_NOT_MET',
]
if response['message'] in temp_error_messages:
raise ContentDecodingError(response['message'])
@property
def fee(self) -> float:
# 0.25 %: See https://bittrex.com/fees
return 0.0025
def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({currency})'.format(
message=data['message'],
currency=currency))
return float(data['result']['Balance'] or 0.0)
def get_balances(self):
data = _API.get_balances()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return data['result']
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self.cached_ticker.keys():
data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
keys = ['Bid', 'Ask', 'Last']
if not data.get('result') or\
not all(key in data.get('result', {}) for key in keys) or\
not all(data.get('result', {})[key] is not None for key in keys):
raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format(
pair=pair))
# Update the pair
self.cached_ticker[pair] = {
'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']),
}
return self.cached_ticker[pair]
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
if tick_interval == 1:
interval = 'oneMin'
elif tick_interval == 5:
interval = 'fiveMin'
elif tick_interval == 30:
interval = 'thirtyMin'
elif tick_interval == 60:
interval = 'hour'
elif tick_interval == 1440:
interval = 'Day'
else:
raise ValueError('Unknown tick_interval: {}'.format(tick_interval))
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
# These sanity check are necessary because bittrex cannot keep their API stable.
if not data.get('result'):
raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format(
pair=pair))
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
for tick in data['result']:
if prop not in tick.keys():
raise ContentDecodingError('Required property {} not present '
'in response params=({})'.format(prop, pair))
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
return data['result']
def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
data = data['result']
return {
'id': data['OrderUuid'],
'type': data['Type'],
'pair': data['Exchange'].replace('-', '_'),
'opened': data['Opened'],
'rate': data['PricePerUnit'],
'amount': data['Quantity'],
'remaining': data['QuantityRemaining'],
'closed': data['Closed'],
}
def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
def get_pair_detail_url(self, pair: str) -> str:
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
def get_markets(self) -> List[str]:
data = _API.get_markets()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException(data['message'])
return [m['MarketName'].replace('-', '_') for m in data['result']]
def get_market_summaries(self) -> List[Dict]:
data = _API.get_market_summaries()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException(data['message'])
return data['result']
def get_wallet_health(self) -> List[Dict]:
data = _API_V2.get_wallet_health()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException(data['message'])
return [{
'Currency': entry['Health']['Currency'],
'IsActive': entry['Health']['IsActive'],
'LastChecked': entry['Health']['LastChecked'],
'Notice': entry['Currency'].get('Notice'),
} for entry in data['result']]

View File

@@ -1,172 +0,0 @@
from abc import ABC, abstractmethod
from typing import Dict, List, Optional
class Exchange(ABC):
@property
def name(self) -> str:
"""
Name of the exchange.
:return: str representation of the class name
"""
return self.__class__.__name__
@property
def fee(self) -> float:
"""
Fee for placing an order
:return: percentage in float
"""
@abstractmethod
def buy(self, pair: str, rate: float, amount: float) -> str:
"""
Places a limit buy order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to purchase
:return: order_id of the placed buy order
"""
@abstractmethod
def sell(self, pair: str, rate: float, amount: float) -> str:
"""
Places a limit sell order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to sell
:return: order_id of the placed sell order
"""
@abstractmethod
def get_balance(self, currency: str) -> float:
"""
Gets account balance.
:param currency: Currency as str, format: BTC
:return: float
"""
@abstractmethod
def get_balances(self) -> List[dict]:
"""
Gets account balances across currencies
:return: List of dicts, format: [
{
'Currency': str,
'Balance': float,
'Available': float,
'Pending': float,
}
...
]
"""
@abstractmethod
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
"""
Gets ticker for given pair.
:param pair: Pair as str, format: BTC_ETC
:param refresh: Shall we query a new value or a cached value is enough
:return: dict, format: {
'bid': float,
'ask': float,
'last': float
}
"""
@abstractmethod
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
"""
Gets ticker history for given pair.
:param pair: Pair as str, format: BTC_ETC
:param tick_interval: ticker interval in minutes
:return: list, format: [
{
'O': float, (Open)
'H': float, (High)
'L': float, (Low)
'C': float, (Close)
'V': float, (Volume)
'T': datetime, (Time)
'BV': float, (Base Volume)
},
...
]
"""
def get_order(self, order_id: str) -> Dict:
"""
Get order details for the given order_id.
:param order_id: ID as str
:return: dict, format: {
'id': str,
'type': str,
'pair': str,
'opened': str ISO 8601 datetime,
'closed': str ISO 8601 datetime,
'rate': float,
'amount': float,
'remaining': int
}
"""
@abstractmethod
def cancel_order(self, order_id: str) -> None:
"""
Cancels order for given order_id.
:param order_id: ID as str
:return: None
"""
@abstractmethod
def get_pair_detail_url(self, pair: str) -> str:
"""
Returns the market detail url for the given pair.
:param pair: Pair as str, format: BTC_ETC
:return: URL as str
"""
@abstractmethod
def get_markets(self) -> List[str]:
"""
Returns all available markets.
:return: List of all available pairs
"""
@abstractmethod
def get_market_summaries(self) -> List[Dict]:
"""
Returns a 24h market summary for all available markets
:return: list, format: [
{
'MarketName': str,
'High': float,
'Low': float,
'Volume': float,
'Last': float,
'TimeStamp': datetime,
'BaseVolume': float,
'Bid': float,
'Ask': float,
'OpenBuyOrders': int,
'OpenSellOrders': int,
'PrevDay': float,
'Created': datetime
},
...
]
"""
@abstractmethod
def get_wallet_health(self) -> List[Dict]:
"""
Returns a list of all wallet health information
:return: list, format: [
{
'Currency': str,
'IsActive': bool,
'LastChecked': str,
'Notice': str
},
...
"""

View File

@@ -5,8 +5,11 @@ e.g BTC to USD
import logging
import time
from typing import Dict, List
from coinmarketcap import Market
from requests.exceptions import RequestException
from freqtrade.constants import SUPPORTED_FIAT
logger = logging.getLogger(__name__)
@@ -32,7 +35,7 @@ class CryptoFiat(object):
self.price = 0.0
# Private attributes
self._expiration = 0
self._expiration = 0.0
self.crypto_symbol = crypto_symbol.upper()
self.fiat_symbol = fiat_symbol.upper()
@@ -63,21 +66,9 @@ class CryptoToFiatConverter(object):
This object is also a Singleton
"""
__instance = None
_coinmarketcap = None
_coinmarketcap: Market = None
# Constants
SUPPORTED_FIAT = [
"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"
]
CRYPTOMAP = {
'BTC': 'bitcoin',
'ETH': 'ethereum',
'USDT': 'thether'
}
_cryptomap: Dict = {}
def __new__(cls):
if CryptoToFiatConverter.__instance is None:
@@ -89,7 +80,19 @@ class CryptoToFiatConverter(object):
return CryptoToFiatConverter.__instance
def __init__(self) -> None:
self._pairs = []
self._pairs: List[CryptoFiat] = []
self._load_cryptomap()
def _load_cryptomap(self) -> None:
try:
coinlistings = self._coinmarketcap.listings()
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
coinlistings["data"]))
except (ValueError, RequestException) as exception:
logger.error(
"Could not load FIAT Cryptocurrency map for the following problem: %s",
exception
)
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
"""
@@ -99,6 +102,8 @@ class CryptoToFiatConverter(object):
:param fiat_symbol: fiat to convert to
:return: float, value in fiat of the crypto-currency amount
"""
if crypto_symbol == fiat_symbol:
return crypto_amount
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
return float(crypto_amount) * float(price)
@@ -114,7 +119,7 @@ class CryptoToFiatConverter(object):
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
# Get the pair that interest us and return the price in fiat
for pair in self._pairs:
@@ -166,7 +171,7 @@ class CryptoToFiatConverter(object):
fiat = fiat.upper()
return fiat in self.SUPPORTED_FIAT
return fiat in SUPPORTED_FIAT
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
"""
@@ -177,17 +182,24 @@ class CryptoToFiatConverter(object):
"""
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
# No need to convert if both crypto and fiat are the same
if crypto_symbol == fiat_symbol:
return 1.0
if crypto_symbol not in self._cryptomap:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
return 0.0
if crypto_symbol not in self.CRYPTOMAP:
raise ValueError(
'The crypto symbol {} is not supported.'.format(crypto_symbol))
try:
return float(
self._coinmarketcap.ticker(
currency=self.CRYPTOMAP[crypto_symbol],
currency=self._cryptomap[crypto_symbol],
convert=fiat_symbol
)[0]['price_' + fiat_symbol.lower()]
)['data']['quotes'][fiat_symbol.upper()]['price']
)
except BaseException:
except BaseException as exception:
logger.error("Error in _find_price: %s", exception)
return 0.0

View File

@@ -3,7 +3,6 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
"""
import copy
import json
import logging
import time
import traceback
@@ -12,19 +11,19 @@ from typing import Dict, List, Optional, Any, Callable
import arrow
import requests
from cachetools import cached, TTLCache
from cachetools import TTLCache, cached
from freqtrade import (
DependencyException, OperationalException, exchange, persistence, __version__
DependencyException, OperationalException, TemporaryError, persistence, __version__,
)
from freqtrade.analyze import Analyze
from freqtrade import constants
from freqtrade.analyze import Analyze
from freqtrade.exchange import Exchange
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.state import State
logger = logging.getLogger(__name__)
@@ -34,12 +33,11 @@ class FreqtradeBot(object):
This is from here the bot start its logic.
"""
def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None):
def __init__(self, config: Dict[str, Any])-> None:
"""
Init all variables and object the bot need to work
:param config: configuration dict, you can use the Configuration.get_config()
method to get the config dict.
:param db_url: database connector string for sqlalchemy (Optional)
"""
logger.info(
@@ -52,27 +50,22 @@ class FreqtradeBot(object):
# Init objects
self.config = config
self.analyze = None
self.fiat_converter = None
self.rpc = None
self.analyze = Analyze(self.config)
self.fiat_converter = CryptoToFiatConverter()
self.rpc: RPCManager = RPCManager(self)
self.persistence = None
self.exchange = None
self.exchange = Exchange(self.config)
self._init_modules(db_url=db_url)
self._init_modules()
def _init_modules(self, db_url: Optional[str] = None) -> None:
def _init_modules(self) -> None:
"""
Initializes all modules and updates the config
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
# Initialize all modules
self.analyze = Analyze(self.config)
self.fiat_converter = CryptoToFiatConverter()
self.rpc = RPCManager(self)
persistence.init(self.config, db_url)
exchange.init(self.config)
persistence.init(self.config)
# Set initial application state
initial_state = self.config.get('initial_state')
@@ -82,19 +75,16 @@ class FreqtradeBot(object):
else:
self.state = State.STOPPED
def clean(self) -> bool:
def cleanup(self) -> None:
"""
Cleanup the application state und finish all pending tasks
Cleanup pending resources on an already stopped bot
:return: None
"""
self.rpc.send_msg('*Status:* `Stopping trader...`')
logger.info('Stopping trader and cleaning up modules...')
self.state = State.STOPPED
logger.info('Cleaning up modules ...')
self.rpc.cleanup()
persistence.cleanup()
return True
def worker(self, old_state: None) -> State:
def worker(self, old_state: State = None) -> State:
"""
Trading routine that must be run at each loop
:param old_state: the previous service state from the previous call
@@ -103,7 +93,7 @@ class FreqtradeBot(object):
# Log state transition
state = self.state
if state != old_state:
self.rpc.send_msg('*Status:* `{}`'.format(state.name.lower()))
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
logger.info('Changing state to: %s', state.name)
if state == State.STOPPED:
@@ -173,66 +163,74 @@ class FreqtradeBot(object):
self.check_handle_timedout(self.config['unfilledtimeout'])
Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
except TemporaryError as error:
logger.warning('%s, retrying in 30 seconds...', error)
time.sleep(constants.RETRY_TIMEOUT)
except OperationalException:
tb = traceback.format_exc()
hint = 'Issue `/start` if you think it is safe to restart.'
self.rpc.send_msg(
'*Status:* OperationalException:\n```\n{traceback}```{hint}'
.format(
traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.'
)
f'*Status:* OperationalException:\n```\n{tb}```{hint}'
)
logger.exception('OperationalException. Stopping trader ...')
self.state = State.STOPPED
return state_changed
@cached(TTLCache(maxsize=1, ttl=1800))
def _gen_pair_whitelist(self, base_currency: str, key: str = 'BaseVolume') -> List[str]:
def _gen_pair_whitelist(self, base_currency: str, key: str = 'quoteVolume') -> List[str]:
"""
Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str
:param key: sort key (defaults to 'BaseVolume')
:param key: sort key (defaults to 'quoteVolume')
:return: List of pairs
"""
summaries = sorted(
(s for s in exchange.get_market_summaries() if
s['MarketName'].startswith(base_currency)),
key=lambda s: s.get(key) or 0.0,
reverse=True
)
return [s['MarketName'].replace('-', '_') for s in summaries]
if not self.exchange.exchange_has('fetchTickers'):
raise OperationalException(
'Exchange does not support dynamic whitelist.'
'Please edit your config and restart the bot'
)
tickers = self.exchange.get_tickers()
# check length so that we make sure that '/' is actually in the string
tickers = [v for k, v in tickers.items()
if len(k.split('/')) == 2 and k.split('/')[1] == base_currency]
sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key])
pairs = [s['symbol'] for s in sorted_tickers]
return pairs
def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
"""
Check wallet health and remove pair from whitelist if necessary
Check available markets and remove pair from whitelist if necessary
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to
trade
:return: the list of pairs the user wants to trade without the one unavailable or
black_listed
"""
sanitized_whitelist = whitelist
health = exchange.get_wallet_health()
markets = self.exchange.get_markets()
markets = [m for m in markets if m['quote'] == self.config['stake_currency']]
known_pairs = set()
for status in health:
pair = '{}_{}'.format(self.config['stake_currency'], status['Currency'])
for market in markets:
pair = market['symbol']
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []):
continue
# else the pair is valid
known_pairs.add(pair)
# Market is not active
if not status['IsActive']:
if not market['active']:
sanitized_whitelist.remove(pair)
logger.info(
'Ignoring %s from whitelist (reason: %s).',
pair, status.get('Notice') or 'wallet is not active'
'Ignoring %s from whitelist. Market is not active.',
pair
)
# We need to remove pairs that are unknown
final_list = [x for x in sanitized_whitelist if x in known_pairs]
return final_list
def get_target_bid(self, ticker: Dict[str, float]) -> float:
@@ -246,27 +244,75 @@ class FreqtradeBot(object):
balance = self.config['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def _get_trade_stake_amount(self) -> Optional[float]:
stake_amount = self.config['stake_amount']
avaliable_amount = self.exchange.get_balance(self.config['stake_currency'])
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all())
if open_trades >= self.config['max_open_trades']:
logger.warning('Can\'t open a new trade: max number of trades is reached')
return None
return avaliable_amount / (self.config['max_open_trades'] - open_trades)
# Check if stake_amount is fulfilled
if avaliable_amount < stake_amount:
raise DependencyException(
'Available balance(%f %s) is lower than stake amount(%f %s)' % (
avaliable_amount, self.config['stake_currency'],
stake_amount, self.config['stake_currency'])
)
return stake_amount
def _get_min_pair_stake_amount(self, pair: str, price: float) -> Optional[float]:
markets = self.exchange.get_markets()
markets = [m for m in markets if m['symbol'] == pair]
if not markets:
raise ValueError(f'Can\'t get market information for symbol {pair}')
market = markets[0]
if 'limits' not in market:
return None
min_stake_amounts = []
if 'cost' in market['limits'] and 'min' in market['limits']['cost']:
min_stake_amounts.append(market['limits']['cost']['min'])
if 'amount' in market['limits'] and 'min' in market['limits']['amount']:
min_stake_amounts.append(market['limits']['amount']['min'] * price)
if not min_stake_amounts:
return None
amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss
if self.analyze.get_stoploss() is not None:
amount_reserve_percent += self.analyze.get_stoploss()
# it should not be more than 50%
amount_reserve_percent = max(amount_reserve_percent, 0.5)
return min(min_stake_amounts)/amount_reserve_percent
def create_trade(self) -> bool:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend
:param interval: Ticker interval used for Analyze
:return: True if a trade object has been created and persisted, False otherwise
"""
stake_amount = self.config['stake_amount']
interval = self.analyze.get_ticker_interval()
stake_amount = self._get_trade_stake_amount()
if not stake_amount:
return False
stake_currency = self.config['stake_currency']
fiat_currency = self.config['fiat_display_currency']
exc_name = self.exchange.name
logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...',
stake_amount
)
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(self.config['stake_currency']) < stake_amount:
raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency'])
)
# Remove currently opened and latest pairs from whitelist
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
@@ -277,50 +323,56 @@ class FreqtradeBot(object):
if not whitelist:
raise DependencyException('No currency pairs in whitelist')
# Pick pair based on StochRSI buy signals
# Pick pair based on buy signals
for _pair in whitelist:
(buy, sell) = self.analyze.get_signal(_pair, interval)
(buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval)
if buy and not sell:
pair = _pair
break
else:
return False
pair_s = pair.replace('_', '/')
pair_url = self.exchange.get_pair_detail_url(pair)
# Calculate amount
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
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:
logger.warning(
f'Can\'t open a new trade for {pair_s}: stake amount'
f' is too small ({stake_amount} < {min_stake_amount})'
)
return False
amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
order_id = self.exchange.buy(pair, buy_limit, amount)['id']
stake_amount_fiat = self.fiat_converter.convert_amount(
stake_amount,
self.config['stake_currency'],
self.config['fiat_display_currency']
stake_currency,
fiat_currency
)
# Create trade entity and return
self.rpc.send_msg(
'*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
.format(
exchange.get_name().upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit,
stake_amount,
self.config['stake_currency'],
stake_amount_fiat,
self.config['fiat_display_currency']
)
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
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 = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
trade = Trade(
pair=pair,
stake_amount=stake_amount,
amount=amount,
fee=exchange.get_fee(),
fee_open=fee,
fee_close=fee,
open_rate=buy_limit,
open_rate_requested=buy_limit,
open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
exchange=self.exchange.id,
open_order_id=order_id
)
Trade.session.add(trade)
@@ -348,32 +400,91 @@ class FreqtradeBot(object):
Tries to execute a sell trade
:return: True if executed
"""
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Found open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
try:
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Found open order for %s', trade)
order = self.exchange.get_order(trade.open_order_id, trade.pair)
# Try update amount (binance-fix)
try:
new_amount = self.get_real_amount(trade, order)
if order['amount'] != new_amount:
order['amount'] = new_amount
# Fee was applied, so set to 0
trade.fee_open = 0
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
return self.handle_trade(trade)
except OperationalException as exception:
logger.warning("could not update trade amount: %s", exception)
trade.update(order)
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
return self.handle_trade(trade)
except DependencyException as exception:
logger.warning('Unable to sell trade: %s', exception)
return False
def get_real_amount(self, trade: Trade, order: Dict) -> float:
"""
Get real amount for the trade
Necessary for self.exchanges which charge fees in base currency (e.g. binance)
"""
order_amount = order['amount']
# Only run for closed orders
if trade.fee_open == 0 or order['status'] == 'open':
return order_amount
# use fee from order-dict if possible
if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}):
if trade.pair.startswith(order['fee']['currency']):
new_amount = order_amount - order['fee']['cost']
logger.info("Applying fee on amount for %s (from %s to %s) from Order",
trade, order['amount'], new_amount)
return new_amount
# Fallback to Trades
trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair,
trade.open_date)
if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
return order_amount
amount = 0
fee_abs = 0
for exectrade in trades:
amount += exectrade['amount']
if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}):
# only applies if fee is in quote currency!
if trade.pair.startswith(exectrade['fee']['currency']):
fee_abs += exectrade['fee']['cost']
if amount != order_amount:
logger.warning(f"amount {amount} does not match amount {trade.amount}")
raise OperationalException("Half bought? Amounts don't match")
real_amount = amount - fee_abs
if fee_abs != 0:
logger.info(f"""Applying fee on amount for {trade} \
(from {order_amount} to {real_amount}) from Trades""")
return real_amount
def handle_trade(self, trade: Trade) -> bool:
"""
Sells the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold, False otherwise
"""
if not trade.is_open:
raise ValueError('attempt to handle closed trade: {}'.format(trade))
raise ValueError(f'attempt to handle closed trade: {trade}')
logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid']
current_rate = self.exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False)
if self.config.get('experimental', {}).get('use_sell_signal'):
(buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval())
experimental = self.config.get('experimental', {})
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
(buy, sell) = self.analyze.get_signal(self.exchange,
trade.pair, self.analyze.get_ticker_interval())
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
self.execute_sell(trade, current_rate)
@@ -391,22 +502,28 @@ class FreqtradeBot(object):
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try:
order = exchange.get_order(trade.open_order_id)
# FIXME: Somehow the query above returns results
# where the open_order_id is in fact None.
# This is probably because the record got
# updated via /forcesell in a different thread.
if not trade.open_order_id:
continue
order = self.exchange.get_order(trade.open_order_id, trade.pair)
except requests.exceptions.RequestException:
logger.info(
'Cannot query order for %s due to %s',
trade,
traceback.format_exc())
continue
ordertime = arrow.get(order['opened'])
ordertime = arrow.get(order['datetime']).datetime
# Check if trade is still actually open
if int(order['remaining']) == 0:
continue
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
if order['side'] == 'buy' and ordertime < timeoutthreashold:
self.handle_timedout_limit_buy(trade, order)
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
elif order['side'] == 'sell' and ordertime < timeoutthreashold:
self.handle_timedout_limit_sell(trade, order)
# FIX: 20180110, why is cancel.order unconditionally here, whereas
@@ -416,16 +533,14 @@ class FreqtradeBot(object):
"""Buy timeout - cancel order
:return: True if order was fully cancelled
"""
exchange.cancel_order(trade.open_order_id)
pair_s = trade.pair.replace('_', '/')
self.exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
# FIX? do we really need to flush, caller of
# check_handle_timedout will flush afterwards
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
self.rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
return True
# if trade is partially complete, edit the stake details for the trade
@@ -434,8 +549,7 @@ class FreqtradeBot(object):
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
return False
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
@@ -444,16 +558,16 @@ class FreqtradeBot(object):
Sell timeout - cancel order and update trade
:return: True if order was fully cancelled
"""
pair_s = trade.pair.replace('_', '/')
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
self.exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
self.rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
trade.pair.replace('_', '/')))
self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
logger.info('Sell order timeout for %s.', trade)
return True
@@ -467,51 +581,42 @@ class FreqtradeBot(object):
:param limit: limit rate for the sell order
:return: None
"""
exc = trade.exchange
pair = trade.pair
# Execute sell and update trade record
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
trade.open_order_id = order_id
trade.close_rate_requested = limit
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit)
current_rate = exchange.get_ticker(trade.pair, False)['bid']
profit = trade.calc_profit_percent(current_rate)
current_rate = self.exchange.get_ticker(trade.pair)['bid']
profit = trade.calc_profit_percent(limit)
pair_url = self.exchange.get_pair_detail_url(trade.pair)
gain = "profit" if fmt_exp_profit > 0 else "loss"
message = "*{exchange}:* Selling\n" \
"*Current Pair:* [{pair}]({pair_url})\n" \
"*Limit:* `{limit}`\n" \
"*Amount:* `{amount}`\n" \
"*Open Rate:* `{open_rate:.8f}`\n" \
"*Current Rate:* `{current_rate:.8f}`\n" \
"*Profit:* `{profit:.2f}%`" \
"".format(
exchange=trade.exchange,
pair=trade.pair,
pair_url=exchange.get_pair_detail_url(trade.pair),
limit=limit,
open_rate=trade.open_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
profit=round(profit * 100, 2),
)
message = f"*{exc}:* Selling\n" \
f"*Current Pair:* [{pair}]({pair_url})\n" \
f"*Limit:* `{limit}`\n" \
f"*Amount:* `{round(trade.amount, 8)}`\n" \
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
f"*Current Rate:* `{current_rate:.8f}`\n" \
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
""
# For regular case, when the configuration exists
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
stake = self.config['stake_currency']
fiat = self.config['fiat_display_currency']
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
self.config['stake_currency'],
self.config['fiat_display_currency']
stake,
fiat
)
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
'` / {profit_fiat:.3f} {fiat})`' \
''.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade,
coin=self.config['stake_currency'],
profit_fiat=profit_fiat,
fiat=self.config['fiat_display_currency'],
)
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:

View File

@@ -13,7 +13,7 @@ def went_down(series: Series) -> bool:
return series < series.shift(1)
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> type(Series):
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> Series:
magic = pi * sqrt(2) / smoothing
a1 = exp(-magic)
coeff2 = 2 * a1 * cos(magic)

View File

@@ -5,11 +5,14 @@ Read the documentation to know what cli arguments you need.
"""
import logging
import sys
from argparse import Namespace
from typing import List
from freqtrade import OperationalException
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.state import State
logger = logging.getLogger('freqtrade')
@@ -43,24 +46,48 @@ def main(sysargv: List[str]) -> None:
state = None
while 1:
state = freqtrade.worker(old_state=state)
if state == State.RELOAD_CONF:
freqtrade = reconfigure(freqtrade, args)
except KeyboardInterrupt:
logger.info('SIGINT received, aborting ...')
return_code = 0
except OperationalException as e:
logger.error(str(e))
return_code = 2
except BaseException:
logger.exception('Fatal exception!')
finally:
if freqtrade:
freqtrade.clean()
freqtrade.rpc.send_msg('*Status:* `Process died ...`')
freqtrade.cleanup()
sys.exit(return_code)
def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
"""
Cleans up current instance, reloads the configuration and returns the new instance
"""
# Clean up current modules
freqtrade.cleanup()
# Create new instance
freqtrade = FreqtradeBot(Configuration(args).get_config())
freqtrade.rpc.send_msg(
'*Status:* `Config reloaded ...`'.format(
freqtrade.state.name.lower()
)
)
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)

View File

@@ -5,6 +5,7 @@ Various tool function for Freqtrade and scripts
import json
import logging
import re
import gzip
from datetime import datetime
from typing import Dict
@@ -63,12 +64,28 @@ def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
return np.sort(arr, axis=0)
def file_dump_json(filename, data) -> None:
def file_dump_json(filename, data, is_zip=False) -> None:
"""
Dump JSON data into a file
:param filename: file to create
:param data: JSON Data to save
:return:
"""
with open(filename, 'w') as fp:
json.dump(data, fp, default=str)
print(f'dumping json to "{filename}"')
if is_zip:
if not filename.endswith('.gz'):
filename = filename + '.gz'
with gzip.open(filename, 'w') as fp:
json.dump(data, fp, default=str)
else:
with open(filename, 'w') as fp:
json.dump(data, fp, default=str)
def format_ms_time(date: int) -> str:
"""
convert MS date to readable format.
: epoch-string in ms
"""
return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S')

View File

@@ -4,38 +4,59 @@ import gzip
import json
import logging
import os
from typing import Optional, List, Dict, Tuple
from typing import Optional, List, Dict, Tuple, Any
import arrow
from freqtrade import misc
from freqtrade.exchange import get_ticker_history
from user_data.hyperopt_conf import hyperopt_optimize_conf
from freqtrade import misc, constants, OperationalException
from freqtrade.exchange import Exchange
from freqtrade.arguments import TimeRange
logger = logging.getLogger(__name__)
def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -> List[Dict]:
stype, start, stop = timerange
if stype == (None, 'line'):
return tickerlist[stop:]
elif stype == ('line', None):
return tickerlist[0:start]
elif stype == ('index', 'index'):
return tickerlist[start:stop]
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
if not tickerlist:
return tickerlist
return tickerlist
start_index = 0
stop_index = len(tickerlist)
if timerange.starttype == 'line':
stop_index = timerange.startts
if timerange.starttype == 'index':
start_index = timerange.startts
elif timerange.starttype == 'date':
while (start_index < len(tickerlist) and
tickerlist[start_index][0] < timerange.startts * 1000):
start_index += 1
if timerange.stoptype == 'line':
start_index = len(tickerlist) + timerange.stopts
if timerange.stoptype == 'index':
stop_index = timerange.stopts
elif timerange.stoptype == 'date':
while (stop_index > 0 and
tickerlist[stop_index-1][0] > timerange.stopts * 1000):
stop_index -= 1
if start_index > stop_index:
raise ValueError(f'The timerange [{timerange.startts},{timerange.stopts}] is incorrect')
return tickerlist[start_index:stop_index]
def load_tickerdata_file(
datadir: str, pair: str,
ticker_interval: int,
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Optional[List[Dict]]:
ticker_interval: str,
timerange: Optional[TimeRange] = None) -> Optional[List[Dict]]:
"""
Load a pair from file,
:return dict OR empty if unsuccesful
"""
path = make_testdata_path(datadir)
pair_file_string = pair.replace('/', '_')
file = os.path.join(path, '{pair}-{ticker_interval}.json'.format(
pair=pair,
pair=pair_file_string,
ticker_interval=ticker_interval,
))
gzipfile = file + '.gz'
@@ -58,31 +79,38 @@ def load_tickerdata_file(
return pairdata
def load_data(datadir: str, ticker_interval: int,
pairs: Optional[List[str]] = None,
def load_data(datadir: str,
ticker_interval: str,
pairs: List[str],
refresh_pairs: Optional[bool] = False,
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Dict[str, List]:
exchange: Optional[Exchange] = None,
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
"""
Loads ticker history data for the given parameters
:return: dict
"""
result = {}
_pairs = pairs or hyperopt_optimize_conf()['exchange']['pair_whitelist']
# If the user force the refresh of pairs
if refresh_pairs:
logger.info('Download data for all pairs and store them in %s', datadir)
download_pairs(datadir, _pairs, ticker_interval)
if not exchange:
raise OperationalException("Exchange needs to be initialized when "
"calling load_data with refresh_pairs=True")
download_pairs(datadir, exchange, pairs, ticker_interval, timerange=timerange)
for pair in _pairs:
for pair in pairs:
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
if not pairdata:
# download the tickerdata from exchange
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
# and retry reading the pair
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
result[pair] = pairdata
if pairdata:
result[pair] = pairdata
else:
logger.warning(
'No data for pair: "%s", Interval: %s. '
'Use --refresh-pairs-cached to download the data',
pair,
ticker_interval
)
return result
@@ -95,14 +123,20 @@ def make_testdata_path(datadir: str) -> str:
)
def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
def download_pairs(datadir, exchange: Exchange, pairs: List[str],
ticker_interval: str,
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> bool:
"""For each pairs passed in parameters, download the ticker intervals"""
for pair in pairs:
try:
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
download_backtesting_testdata(datadir,
exchange=exchange,
pair=pair,
tick_interval=ticker_interval,
timerange=timerange)
except BaseException:
logger.info(
'Failed to download the pair: "%s", Interval: %s min',
'Failed to download the pair: "%s", Interval: %s',
pair,
ticker_interval
)
@@ -110,39 +144,89 @@ def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
return True
# FIX: 20180110, suggest rename interval to tick_interval
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> None:
def load_cached_data_for_updating(filename: str,
tick_interval: str,
timerange: Optional[TimeRange]) -> Tuple[
List[Any],
Optional[int]]:
"""
Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
Load cached data and choose what part of the data should be updated
"""
path = make_testdata_path(datadir)
logger.info(
'Download the pair: "%s", Interval: %s min', pair, interval
)
since_ms = None
filename = os.path.join(path, '{pair}-{interval}.json'.format(
pair=pair.replace("-", "_"),
interval=interval,
))
# user sets timerange, so find the start time
if timerange:
if timerange.starttype == 'date':
since_ms = timerange.startts * 1000
elif timerange.stoptype == 'line':
num_minutes = timerange.stopts * constants.TICKER_INTERVAL_MINUTES[tick_interval]
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
# read the cached file
if os.path.isfile(filename):
with open(filename, "rt") as file:
data = json.load(file)
# remove the last item, because we are not sure if it is correct
# it could be fetched when the candle was incompleted
if data:
data.pop()
else:
data = []
logger.debug('Current Start: %s', data[1]['T'] if data else None)
logger.debug('Current End: %s', data[-1:][0]['T'] if data else None)
if data:
if since_ms and since_ms < data[0][0]:
# the data is requested for earlier period than the cache has
# so fully redownload all the data
data = []
else:
# a part of the data was already downloaded, so
# download unexist data only
since_ms = data[-1][0] + 1
# Extend data with new ticker history
data.extend([
row for row in get_ticker_history(pair=pair, tick_interval=int(interval))
if row not in data
])
return (data, since_ms)
def download_backtesting_testdata(datadir: str,
exchange: Exchange,
pair: str,
tick_interval: str = '5m',
timerange: Optional[TimeRange] = None) -> None:
"""
Download the latest ticker intervals from the exchange for the pairs passed in parameters
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,
the full data will be redownloaded
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
:param pairs: list of pairs to download
:param tick_interval: ticker interval
:param timerange: range of time to download
:return: None
"""
path = make_testdata_path(datadir)
filepair = pair.replace("/", "_")
filename = os.path.join(path, f'{filepair}-{tick_interval}.json')
logger.info(
'Download the pair: "%s", Interval: %s',
pair,
tick_interval
)
data, since_ms = load_cached_data_for_updating(filename, tick_interval, timerange)
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')
new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval,
since_ms=since_ms)
data.extend(new_data)
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
data = sorted(data, key=lambda _data: _data['T'])
logger.debug('New Start: %s', data[1]['T'])
logger.debug('New End: %s', data[-1:][0]['T'])
misc.file_dump_json(filename, data)

View File

@@ -6,25 +6,40 @@ This module contains the backtesting logic
import logging
import operator
from argparse import Namespace
from typing import Dict, Tuple, Any, List, Optional
from datetime import datetime
from typing import Dict, Tuple, Any, List, Optional, NamedTuple
import arrow
from pandas import DataFrame
from tabulate import tabulate
import freqtrade.optimize as optimize
from freqtrade import exchange
from freqtrade import constants, DependencyException
from freqtrade.exchange import Exchange
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.exchange import Bittrex
from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
class BacktestResult(NamedTuple):
"""
NamedTuple Defining BacktestResults inputs.
"""
pair: str
profit_percent: float
profit_abs: float
open_time: datetime
close_time: datetime
open_index: int
close_index: int
trade_duration: float
open_at_end: bool
class Backtesting(object):
"""
Backtesting class, this class contains all the logic to run a backtest
@@ -35,24 +50,20 @@ class Backtesting(object):
"""
def __init__(self, config: Dict[str, Any]) -> None:
self.config = config
self.analyze = None
self.ticker_interval = None
self.tickerdata_to_dataframe = None
self.populate_buy_trend = None
self.populate_sell_trend = None
self._init()
def _init(self) -> None:
"""
Init objects required for backtesting
:return: None
"""
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
exchange._API = Bittrex({'key': '', 'secret': ''})
# Reset keys for backtesting
self.config['exchange']['key'] = ''
self.config['exchange']['secret'] = ''
self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = ''
self.config['dry_run'] = True
self.exchange = Exchange(self.config)
self.fee = self.exchange.get_fee()
@staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
@@ -73,22 +84,22 @@ class Backtesting(object):
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
stake_currency = self.config.get('stake_currency')
stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
tabular_data = []
headers = ['pair', 'buy count', 'avg profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data:
result = results[results.currency == pair]
result = results[results.pair == pair]
tabular_data.append([
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_BTC.sum(),
result.duration.mean(),
len(result[result.profit_BTC > 0]),
len(result[result.profit_BTC < 0])
result.profit_abs.sum(),
result.trade_duration.mean(),
len(result[result.profit_abs > 0]),
len(result[result.profit_abs < 0])
])
# Append Total
@@ -96,16 +107,28 @@ class Backtesting(object):
'TOTAL',
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(),
results.duration.mean(),
len(results[results.profit_BTC > 0]),
len(results[results.profit_BTC < 0])
results.profit_abs.sum(),
results.trade_duration.mean(),
len(results[results.profit_abs > 0]),
len(results[results.profit_abs < 0])
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
records = [(trade_entry.pair, trade_entry.profit_percent,
trade_entry.open_time.timestamp(),
trade_entry.close_time.timestamp(),
trade_entry.open_index - 1, trade_entry.trade_duration)
for index, trade_entry in results.iterrows()]
if records:
logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records)
def _get_sell_trade_entry(
self, pair: str, buy_row: DataFrame,
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[Tuple]:
partial_ticker: List, trade_count_lock: Dict, args: Dict) -> Optional[BacktestResult]:
stake_amount = args['stake_amount']
max_open_trades = args.get('max_open_trades', 0)
@@ -114,7 +137,8 @@ class Backtesting(object):
open_date=buy_row.date,
stake_amount=stake_amount,
amount=stake_amount / buy_row.open,
fee=exchange.get_fee()
fee_open=self.fee,
fee_close=self.fee
)
# calculate win/lose forwards from buy point
@@ -126,15 +150,33 @@ class Backtesting(object):
buy_signal = sell_row.buy
if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal,
sell_row.sell):
return \
sell_row, \
(
pair,
trade.calc_profit_percent(rate=sell_row.close),
trade.calc_profit(rate=sell_row.close),
(sell_row.date - buy_row.date).seconds // 60
), \
sell_row.date
return BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
profit_abs=trade.calc_profit(rate=sell_row.close),
open_time=buy_row.date,
close_time=sell_row.date,
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index,
close_index=sell_row.Index,
open_at_end=False
)
if partial_ticker:
# no sell condition found - trade stil open at end of backtest period
sell_row = partial_ticker[-1]
btr = BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.close),
profit_abs=trade.calc_profit(rate=sell_row.close),
open_time=buy_row.date,
close_time=sell_row.date,
trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index,
close_index=sell_row.Index,
open_at_end=True
)
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
btr.profit_percent, btr.profit_abs)
return btr
return None
def backtest(self, args: Dict) -> DataFrame:
@@ -150,22 +192,28 @@ class Backtesting(object):
processed: a processed dictionary with format {pair, data}
max_open_trades: maximum number of concurrent trades (default: 0, disabled)
realistic: do we try to simulate realistic trades? (default: True)
sell_profit_only: sell if profit only
use_sell_signal: act on sell-signal
:return: DataFrame
"""
headers = ['date', 'buy', 'open', 'close', 'sell']
processed = args['processed']
max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', False)
record = args.get('record', None)
records = []
trades = []
trade_count_lock = {}
trade_count_lock: Dict = {}
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = self.populate_sell_trend(self.populate_buy_trend(pair_data))[headers]
ticker_data = self.populate_sell_trend(
self.populate_buy_trend(pair_data))[headers].copy()
# 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[:, 'sell'] = ticker_data['sell'].shift(1)
ticker_data.drop(ticker_data.head(1).index, inplace=True)
# Convert from Pandas to list for performance reasons
# (Looping Pandas is slow.)
ticker = [x for x in ticker_data.itertuples()]
lock_pair_until = None
@@ -183,28 +231,18 @@ class Backtesting(object):
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
ret = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
trade_count_lock, args)
trade_entry = self._get_sell_trade_entry(pair, row, ticker[index + 1:],
trade_count_lock, args)
if ret:
row2, trade_entry, next_date = ret
lock_pair_until = next_date
if trade_entry:
lock_pair_until = trade_entry.close_time
trades.append(trade_entry)
if record:
# Note, need to be json.dump friendly
# record a tuple of pair, current_profit_percent,
# entry-date, duration
records.append((pair, trade_entry[1],
row.date.strftime('%s'),
row2.date.strftime('%s'),
index, trade_entry[3]))
# For now export inside backtest(), maybe change so that backtest()
# returns a tuple like: (dataframe, records, logs, etc)
if record and record.find('trades') >= 0:
logger.info('Dumping backtest results')
file_dump_json('backtest-result.json', records)
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
return DataFrame.from_records(trades, columns=labels)
else:
# Set lock_pair_until to end of testing period if trade could not be closed
# This happens only if the buy-signal was with the last candle
lock_pair_until = ticker_data.iloc[-1].date
return DataFrame.from_records(trades, columns=BacktestResult._fields)
def start(self) -> None:
"""
@@ -219,19 +257,24 @@ class Backtesting(object):
if self.config.get('live'):
logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = exchange.get_ticker_history(pair, self.ticker_interval)
data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval)
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
timerange = Arguments.parse_timerange(self.config.get('timerange'))
timerange = Arguments.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = optimize.load_data(
self.config['datadir'],
pairs=pairs,
ticker_interval=self.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.exchange,
timerange=timerange
)
if not data:
logger.critical("No data found. Terminating.")
return
# Ignore max_open_trades in backtesting, except realistic flag was passed
if self.config.get('realistic_simulation', False):
max_open_trades = self.config['max_open_trades']
@@ -251,23 +294,22 @@ class Backtesting(object):
)
# Execute backtest and print results
sell_profit_only = self.config.get('experimental', {}).get('sell_profit_only', False)
use_sell_signal = self.config.get('experimental', {}).get('use_sell_signal', False)
results = self.backtest(
{
'stake_amount': self.config.get('stake_amount'),
'processed': preprocessed,
'max_open_trades': max_open_trades,
'realistic': self.config.get('realistic_simulation', False),
'sell_profit_only': sell_profit_only,
'use_sell_signal': use_sell_signal,
'record': self.config.get('export')
}
)
if self.config.get('export', False):
self._store_backtest_result(self.config.get('exportfilename'), results)
logger.info(
'\n==================================== '
'\n======================================== '
'BACKTESTING REPORT'
' ====================================\n'
' =========================================\n'
'%s',
self._generate_text_table(
data,
@@ -275,6 +317,17 @@ class Backtesting(object):
)
)
logger.info(
'\n====================================== '
'LEFT OPEN TRADES REPORT'
' ======================================\n'
'%s',
self._generate_text_table(
data,
results.loc[results.open_at_end]
)
)
def setup_configuration(args: Namespace) -> Dict[str, Any]:
"""
@@ -289,6 +342,10 @@ def setup_configuration(args: Namespace) -> Dict[str, Any]:
config['exchange']['key'] = ''
config['exchange']['secret'] = ''
if config['stake_amount'] == constants.UNLIMITED_STAKE_AMOUNT:
raise DependencyException('stake amount could not be "%s" for backtesting' %
constants.UNLIMITED_STAKE_AMOUNT)
return config

View File

@@ -14,12 +14,11 @@ from argparse import Namespace
from functools import reduce
from math import exp
from operator import itemgetter
from typing import Dict, Any, Callable
from typing import Dict, Any, Callable, Optional
import numpy
import talib.abstract as ta
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
from hyperopt.mongoexp import MongoTrials
from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib
@@ -27,8 +26,6 @@ from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.optimize import load_data
from freqtrade.optimize.backtesting import Backtesting
from user_data.hyperopt_conf import hyperopt_optimize_conf
logger = logging.getLogger(__name__)
@@ -42,7 +39,6 @@ class Hyperopt(Backtesting):
hyperopt.start()
"""
def __init__(self, config: Dict[str, Any]) -> None:
super().__init__(config)
# set TARGET_TRADES to suit your number concurrent trades so its realistic
# to the number of days
@@ -61,7 +57,7 @@ class Hyperopt(Backtesting):
self.expected_max_profit = 3.0
# Configuration and data used by hyperopt
self.processed = None
self.processed: Optional[Dict[str, Any]] = None
# Hyperopt Trials
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
@@ -345,7 +341,7 @@ class Hyperopt(Backtesting):
"""
Return the space to use during Hyperopt
"""
spaces = {}
spaces: Dict = {}
if self.has_space('buy'):
spaces = {**spaces, **Hyperopt.indicator_space()}
if self.has_space('roi'):
@@ -452,10 +448,11 @@ class Hyperopt(Backtesting):
total_profit = results.profit_percent.sum()
trade_count = len(results.index)
trade_duration = results.duration.mean()
trade_duration = results.trade_duration.mean()
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
print('.', end='')
sys.stdout.flush()
return {
'status': STATUS_FAIL,
'loss': float('inf')
@@ -480,59 +477,48 @@ class Hyperopt(Backtesting):
'result': result_explanation,
}
@staticmethod
def format_results(results: DataFrame) -> str:
def format_results(self, results: DataFrame) -> str:
"""
Return the format result in a string
"""
return ('{:6d} trades. Avg profit {: 5.2f}%. '
'Total profit {: 11.8f} BTC ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(),
results.profit_abs.sum(),
self.config['stake_currency'],
results.profit_percent.sum(),
results.duration.mean(),
results.trade_duration.mean(),
)
def start(self) -> None:
timerange = Arguments.parse_timerange(self.config.get('timerange'))
timerange = Arguments.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = load_data(
datadir=self.config.get('datadir'),
datadir=str(self.config.get('datadir')),
pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.ticker_interval,
timerange=timerange
)
if self.has_space('buy'):
self.analyze.populate_indicators = Hyperopt.populate_indicators
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
self.processed = self.tickerdata_to_dataframe(data)
if self.config.get('mongodb'):
logger.info('Using mongodb ...')
logger.info('Preparing Trials..')
signal.signal(signal.SIGINT, self.signal_handler)
# 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(
'Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!'
'Continuing with trials. Current: %d, Total: %d',
self.current_tries,
self.total_tries
)
db_name = 'freqtrade_hyperopt'
self.trials = MongoTrials(
arg='mongo://127.0.0.1:1234/{}/jobs'.format(db_name),
exp_key='exp1'
)
else:
logger.info('Preparing Trials..')
signal.signal(signal.SIGINT, self.signal_handler)
# 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
)
try:
best_parameters = fmin(
fn=self.generate_optimizer,
@@ -588,18 +574,14 @@ def start(args: Namespace) -> None:
"""
# Remove noisy log messages
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
# Initialize configuration
# Monkey patch the configuration with hyperopt_conf.py
configuration = Configuration(args)
logger.info('Starting freqtrade in Hyperopt mode')
config = configuration.load_config()
optimize_config = hyperopt_optimize_conf()
config = configuration._load_common_config(optimize_config)
config = configuration._load_backtesting_config(config)
config = configuration._load_hyperopt_config(config)
config['exchange']['key'] = ''
config['exchange']['secret'] = ''

View File

@@ -5,57 +5,111 @@ This module contains the class to persist trades into SQLite
import logging
from datetime import datetime
from decimal import Decimal, getcontext
from typing import Dict, Optional
from typing import Dict, Optional, Any
import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
create_engine)
from sqlalchemy.engine import Engine
from sqlalchemy import inspect
from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool
from freqtrade import OperationalException
logger = logging.getLogger(__name__)
_CONF = {}
_DECL_BASE = declarative_base()
_DECL_BASE: Any = declarative_base()
def init(config: dict, engine: Optional[Engine] = None) -> None:
def init(config: Dict) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:param engine: database engine for sqlalchemy (Optional)
:return: None
"""
_CONF.update(config)
if not engine:
if _CONF.get('dry_run', False):
# the user wants dry run to use a DB
if _CONF.get('dry_run_db', False):
engine = create_engine('sqlite:///tradesv3.dry_run.sqlite')
# Otherwise dry run will store in memory
else:
engine = create_engine('sqlite://',
connect_args={'check_same_thread': False},
poolclass=StaticPool,
echo=False)
else:
engine = create_engine('sqlite:///tradesv3.sqlite')
db_url = config.get('db_url', None)
kwargs = {}
# Take care of thread ownership if in-memory db
if db_url == 'sqlite://':
kwargs.update({
'connect_args': {'check_same_thread': False},
'poolclass': StaticPool,
'echo': False,
})
try:
engine = create_engine(db_url, **kwargs)
except NoSuchModuleError:
error = 'Given value for db_url: \'{}\' is no valid database URL! (See {}).'.format(
db_url, 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
)
raise OperationalException(error)
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session()
Trade.query = session.query_property()
_DECL_BASE.metadata.create_all(engine)
check_migrate(engine)
# Clean dry_run DB
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
# Clean dry_run DB if the db is not in-memory
if config.get('dry_run', False) and db_url != 'sqlite://':
clean_dry_run_db()
def has_column(columns, searchname: str) -> bool:
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
def check_migrate(engine) -> None:
"""
Checks if migration is necessary and migrates if necessary
"""
inspector = inspect(engine)
cols = inspector.get_columns('trades')
if not has_column(cols, 'fee_open'):
# Schema migration necessary
engine.execute("alter table trades rename to trades_bak")
# let SQLAlchemy create the schema as required
_DECL_BASE.metadata.create_all(engine)
# Copy data back - following the correct schema
engine.execute("""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id)
select id, lower(exchange),
case
when instr(pair, '_') != 0 then
substr(pair, instr(pair, '_') + 1) || '/' ||
substr(pair, 1, instr(pair, '_') - 1)
else pair
end
pair,
is_open, fee fee_open, fee fee_close,
open_rate, null open_rate_requested, close_rate,
null close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id
from trades_bak
""")
# Reread columns - the above recreated the table!
inspector = inspect(engine)
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:
"""
Flushes all pending operations to disk.
@@ -85,9 +139,12 @@ class Trade(_DECL_BASE):
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True)
fee = 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)
open_rate = Column(Float)
open_rate_requested = Column(Float)
close_rate = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float)
stake_amount = Column(Float, nullable=False)
amount = Column(Float)
@@ -111,20 +168,20 @@ class Trade(_DECL_BASE):
:return: None
"""
# Ignore open and cancelled orders
if not order['closed'] or order['rate'] is None:
if order['status'] == 'open' or order['price'] is None:
return
logger.info('Updating trade (id=%d) ...', self.id)
getcontext().prec = 8 # Bittrex do not go above 8 decimal
if order['type'] == 'LIMIT_BUY':
if order['type'] == 'limit' and order['side'] == 'buy':
# Update open rate and actual amount
self.open_rate = Decimal(order['rate'])
self.open_rate = Decimal(order['price'])
self.amount = Decimal(order['amount'])
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
self.open_order_id = None
elif order['type'] == 'LIMIT_SELL':
self.close(order['rate'])
elif order['type'] == 'limit' and order['side'] == 'sell':
self.close(order['price'])
else:
raise ValueError('Unknown order type: {}'.format(order['type']))
cleanup()
@@ -156,7 +213,7 @@ class Trade(_DECL_BASE):
getcontext().prec = 8
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
fees = buy_trade * Decimal(fee or self.fee)
fees = buy_trade * Decimal(fee or self.fee_open)
return float(buy_trade + fees)
def calc_close_trade_price(
@@ -177,7 +234,7 @@ class Trade(_DECL_BASE):
return 0.0
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
fees = sell_trade * Decimal(fee or self.fee)
fees = sell_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees)
def calc_profit(
@@ -195,7 +252,7 @@ class Trade(_DECL_BASE):
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=(rate or self.close_rate),
fee=(fee or self.fee)
fee=(fee or self.fee_close)
)
return float("{0:.8f}".format(close_trade_price - open_trade_price))
@@ -215,7 +272,7 @@ class Trade(_DECL_BASE):
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=(rate or self.close_rate),
fee=(fee or self.fee)
fee=(fee or self.fee_close)
)
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))

View File

@@ -2,23 +2,33 @@
This module contains class to define a RPC communications
"""
import logging
from datetime import datetime, timedelta
from abc import abstractmethod
from datetime import datetime, timedelta, date
from decimal import Decimal
from typing import Tuple, Any
from typing import Dict, Tuple, Any, List
import arrow
import sqlalchemy as sql
from numpy import mean, nan_to_num
from pandas import DataFrame
from freqtrade import exchange
from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade
from freqtrade.state import State
logger = logging.getLogger(__name__)
class RPCException(Exception):
"""
Should be raised with a rpc-formatted message in an _rpc_* method
if the required state is wrong, i.e.:
raise RPCException('*Status:* `no active trade`')
"""
pass
class RPC(object):
"""
RPC class can be used to have extra feature, like bot data, and access to DB data
@@ -29,28 +39,40 @@ class RPC(object):
:param freqtrade: Instance of a freqtrade bot
:return: None
"""
self.freqtrade = freqtrade
self._freqtrade = freqtrade
def rpc_trade_status(self) -> Tuple[bool, Any]:
@abstractmethod
def cleanup(self) -> None:
""" Cleanup pending module resources """
@property
@abstractmethod
def name(self) -> str:
""" Returns the lowercase name of this module """
@abstractmethod
def send_msg(self, msg: str) -> None:
""" Sends a message to all registered rpc modules """
def _rpc_trade_status(self) -> List[str]:
"""
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function
:return:
"""
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.state != State.RUNNING:
return True, '*Status:* `trader is not running`'
if self._freqtrade.state != State.RUNNING:
raise RPCException('*Status:* `trader is not running`')
elif not trades:
return True, '*Status:* `no active trade`'
raise RPCException('*Status:* `no active trade`')
else:
result = []
for trade in trades:
order = None
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
# calculate profit and send message to user
current_rate = 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)
fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit * 100, 2)
@@ -68,7 +90,7 @@ class RPC(object):
.format(
trade_id=trade.id,
pair=trade.pair,
market_url=exchange.get_pair_detail_url(trade.pair),
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
date=arrow.get(trade.open_date).humanize(),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
@@ -76,24 +98,24 @@ class RPC(object):
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['remaining']
open_order='({} {} rem={:.8f})'.format(
order['type'], order['side'], order['remaining']
) if order else None,
)
result.append(message)
return False, result
return result
def rpc_status_table(self) -> Tuple[bool, Any]:
def _rpc_status_table(self) -> DataFrame:
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.state != State.RUNNING:
return True, '*Status:* `trader is not running`'
if self._freqtrade.state != State.RUNNING:
raise RPCException('*Status:* `trader is not running`')
elif not trades:
return True, '*Status:* `no active order`'
raise RPCException('*Status:* `no active order`')
else:
trades_list = []
for trade in trades:
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
trades_list.append([
trade.id,
trade.pair,
@@ -104,22 +126,18 @@ class RPC(object):
columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0])
# The style used throughout is to return a tuple
# consisting of (error_occured?, result)
# Another approach would be to just return the
# result, or raise error
return False, df_statuses
return df_statuses
def rpc_daily_profit(
def _rpc_daily_profit(
self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
stake_currency: str, fiat_display_currency: str) -> List[List[Any]]:
today = datetime.utcnow().date()
profit_days = {}
profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0):
return True, '*Daily [n]:* `must be an integer greater than 0`'
raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
fiat = self.freqtrade.fiat_converter
fiat = self._freqtrade.fiat_converter
for day in range(0, timescale):
profitday = today - timedelta(days=day)
trades = Trade.query \
@@ -134,7 +152,7 @@ class RPC(object):
'trades': len(trades)
}
stats = [
return [
[
key,
'{value:.8f} {symbol}'.format(
@@ -156,13 +174,10 @@ class RPC(object):
]
for key, value in profit_days.items()
]
return False, stats
def rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
"""
:return: cumulative profit statistics.
"""
def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
""" Returns cumulative profit statistics """
trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = []
@@ -172,7 +187,7 @@ class RPC(object):
durations = []
for trade in trades:
current_rate = None
current_rate: float = 0.0
if not trade.open_rate:
continue
@@ -185,7 +200,7 @@ class RPC(object):
profit_closed_percent.append(profit_percent)
else:
# Get current rate
current_rate = exchange.get_ticker(trade.pair, False)['bid']
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_coin.append(
@@ -200,141 +215,140 @@ class RPC(object):
.order_by(sql.text('profit_sum DESC')).first()
if not best_pair:
return True, '*Status:* `no closed trade`'
raise RPCException('*Status:* `no closed trade`')
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
fiat = self._freqtrade.fiat_converter
# Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(sum(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_coin,
stake_currency,
fiat_display_currency
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(sum(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_coin,
stake_currency,
fiat_display_currency
)
num = float(len(durations) or 1)
return (
False,
{
'profit_closed_coin': profit_closed_coin,
'profit_closed_percent': profit_closed_percent,
'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin,
'profit_all_percent': profit_all_percent,
'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades),
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'best_pair': bp_pair,
'best_rate': round(bp_rate * 100, 2)
}
)
def rpc_balance(self, fiat_display_currency: str) -> Tuple[bool, Any]:
"""
:return: current account balance per crypto
"""
balances = [
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
return True, '`All balances are zero.`'
return {
'profit_closed_coin': profit_closed_coin,
'profit_closed_percent': profit_closed_percent,
'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin,
'profit_all_percent': profit_all_percent,
'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades),
'first_trade_date': arrow.get(trades[0].open_date).humanize(),
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'best_pair': bp_pair,
'best_rate': round(bp_rate * 100, 2),
}
def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
""" Returns current account balance per crypto """
output = []
total = 0.0
for currency in balances:
coin = currency['Currency']
for coin, balance in self._freqtrade.exchange.get_balances().items():
if not balance['total']:
continue
if coin == 'BTC':
currency["Rate"] = 1.0
rate = 1.0
else:
if coin == 'USDT':
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid']
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
else:
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
currency['BTC'] = currency["Rate"] * currency["Balance"]
total = total + currency['BTC']
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
est_btc: float = rate * balance['total']
total = total + est_btc
output.append(
{
'currency': currency['Currency'],
'available': currency['Available'],
'balance': currency['Balance'],
'pending': currency['Pending'],
'est_btc': currency['BTC']
'currency': coin,
'available': balance['free'],
'balance': balance['total'],
'pending': balance['used'],
'est_btc': est_btc
}
)
fiat = self.freqtrade.fiat_converter
if total == 0.0:
raise RPCException('`All balances are zero.`')
fiat = self._freqtrade.fiat_converter
symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol)
return False, (output, total, symbol, value)
return output, total, symbol, value
def rpc_start(self) -> (bool, str):
"""
Handler for start.
"""
if self.freqtrade.state == State.RUNNING:
return True, '*Status:* `already running`'
def _rpc_start(self) -> str:
""" Handler for start """
if self._freqtrade.state == State.RUNNING:
return '*Status:* `already running`'
self.freqtrade.state = State.RUNNING
return False, '`Starting trader ...`'
self._freqtrade.state = State.RUNNING
return '`Starting trader ...`'
def rpc_stop(self) -> (bool, str):
"""
Handler for stop.
"""
if self.freqtrade.state == State.RUNNING:
self.freqtrade.state = State.STOPPED
return False, '`Stopping trader ...`'
def _rpc_stop(self) -> str:
""" Handler for stop """
if self._freqtrade.state == State.RUNNING:
self._freqtrade.state = State.STOPPED
return '`Stopping trader ...`'
return True, '*Status:* `already stopped`'
return '*Status:* `already stopped`'
def _rpc_reload_conf(self) -> str:
""" Handler for reload_conf. """
self._freqtrade.state = State.RELOAD_CONF
return '*Status:* `Reloading config ...`'
# FIX: no test for this!!!!
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]:
def _rpc_forcesell(self, trade_id) -> None:
"""
Handler for forcesell <id>.
Sells the given trade at current price
:return: error or None
"""
def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair)
# Cancel open LIMIT_BUY orders and close trade
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
exchange.cancel_order(trade.open_order_id)
trade.close(order.get('rate') or trade.open_rate)
# TODO: sell amount which has been bought already
return
if order and order['status'] == 'open' \
and order['type'] == 'limit' \
and order['side'] == 'buy':
self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close(order.get('price') or trade.open_rate)
# Do the best effort, if we don't know 'filled' amount, don't try selling
if order['filled'] is None:
return
trade.amount = order['filled']
# Ignore trades with an attached LIMIT_SELL order
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
if order and order['status'] == 'open' \
and order['type'] == 'limit' \
and order['side'] == 'sell':
return
# Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid']
self.freqtrade.execute_sell(trade, current_rate)
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
self._freqtrade.execute_sell(trade, current_rate)
# ---- EOF def _exec_forcesell ----
if self.freqtrade.state != State.RUNNING:
return True, '`trader is not running`'
if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`')
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade)
return False, ''
return
# Query for trade
trade = Trade.query.filter(
@@ -345,18 +359,18 @@ class RPC(object):
).first()
if not trade:
logger.warning('forcesell: Invalid argument received')
return True, 'Invalid argument.'
raise RPCException('Invalid argument.')
_exec_forcesell(trade)
return False, ''
Trade.session.flush()
def rpc_performance(self) -> Tuple[bool, Any]:
def _rpc_performance(self) -> List[Dict]:
"""
Handler for performance.
Shows a performance statistic from finished trades
"""
if self.freqtrade.state != State.RUNNING:
return True, '`trader is not running`'
if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`')
pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'),
@@ -365,19 +379,14 @@ class RPC(object):
.group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \
.all()
trades = []
for (pair, rate, count) in pair_rates:
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count})
return [
{'pair': pair, 'profit': round(rate * 100, 2), 'count': count}
for pair, rate, count in pair_rates
]
return False, trades
def _rpc_count(self) -> List[Trade]:
""" Returns the number of trades running """
if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`')
def rpc_count(self) -> Tuple[bool, Any]:
"""
Returns the number of trades running
:return: None
"""
if self.freqtrade.state != State.RUNNING:
return True, '`trader is not running`'
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
return False, trades
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, ...)
"""
import logging
from typing import List
from freqtrade.rpc.telegram import Telegram
from freqtrade.rpc.rpc import RPC
logger = logging.getLogger(__name__)
@@ -14,36 +14,23 @@ class RPCManager(object):
Class to manage RPC objects (Telegram, Slack, ...)
"""
def __init__(self, freqtrade) -> None:
"""
Initializes all enabled rpc modules
:param config: config to use
:return: None
"""
self.freqtrade = freqtrade
""" Initializes all enabled rpc modules """
self.registered_modules: List[RPC] = []
self.registered_modules = []
self.telegram = None
self._init()
def _init(self) -> None:
"""
Init RPC modules
:return:
"""
if self.freqtrade.config['telegram'].get('enabled', False):
# Enable telegram
if freqtrade.config['telegram'].get('enabled', False):
logger.info('Enabling rpc.telegram ...')
self.registered_modules.append('telegram')
self.telegram = Telegram(self.freqtrade)
from freqtrade.rpc.telegram import Telegram
self.registered_modules.append(Telegram(freqtrade))
def cleanup(self) -> None:
"""
Stops all enabled rpc modules
:return: None
"""
if 'telegram' in self.registered_modules:
logger.info('Cleaning up rpc.telegram ...')
self.registered_modules.remove('telegram')
self.telegram.cleanup()
""" Stops all enabled rpc modules """
logger.info('Cleaning up rpc modules ...')
while self.registered_modules:
mod = self.registered_modules.pop()
logger.debug('Cleaning up rpc.%s ...', mod.name)
mod.cleanup()
del mod
def send_msg(self, msg: str) -> None:
"""
@@ -51,6 +38,7 @@ class RPCManager(object):
:param msg: message
:return: None
"""
logger.info(msg)
if 'telegram' in self.registered_modules:
self.telegram.send_msg(msg)
logger.info('Sending rpc message: %s', msg)
for mod in self.registered_modules:
logger.debug('Forwarding message to rpc.%s', mod.name)
mod.send_msg(msg)

View File

@@ -12,22 +12,21 @@ from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater
from freqtrade.__init__ import __version__
from freqtrade.rpc.rpc import RPC
from freqtrade.rpc.rpc import RPC, RPCException
logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...')
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
"""
Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler
:return: decorated function
"""
def wrapper(self, *args, **kwargs):
"""
Decorator logic
"""
""" Decorator logic """
update = kwargs.get('update') or args[1]
# Reject unauthorized messages
@@ -54,9 +53,12 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
class Telegram(RPC):
"""
Telegram, this class send messages to Telegram
"""
""" This class handles all telegram communication """
@property
def name(self) -> str:
return "telegram"
def __init__(self, freqtrade) -> None:
"""
Init the Telegram call, and init the super class RPC
@@ -65,7 +67,7 @@ class Telegram(RPC):
"""
super().__init__(freqtrade)
self._updater = None
self._updater: Updater = None
self._config = freqtrade.config
self._init()
@@ -74,12 +76,7 @@ class Telegram(RPC):
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:return: None
"""
if not self.is_enabled():
return
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
# Register command handler and start telegram message polling
@@ -93,6 +90,7 @@ class Telegram(RPC):
CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
CommandHandler('reload_conf', self._reload_conf),
CommandHandler('help', self._help),
CommandHandler('version', self._version),
]
@@ -114,16 +112,11 @@ class Telegram(RPC):
Stops all running telegram threads.
:return: None
"""
if not self.is_enabled():
return
self._updater.stop()
def is_enabled(self) -> bool:
"""
Returns True if the telegram module is activated, False otherwise
"""
return bool(self._config.get('telegram', {}).get('enabled', False))
def send_msg(self, msg: str) -> None:
""" Send a message to telegram channel """
self._send_msg(msg)
@authorized_only
def _status(self, bot: Bot, update: Update) -> None:
@@ -142,13 +135,11 @@ class Telegram(RPC):
self._status_table(bot, update)
return
# Fetch open trade
(error, trades) = self.rpc_trade_status()
if error:
self.send_msg(trades, bot=bot)
else:
for trademsg in trades:
self.send_msg(trademsg, bot=bot)
try:
for trade_msg in self._rpc_trade_status():
self._send_msg(trade_msg, bot=bot)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _status_table(self, bot: Bot, update: Update) -> None:
@@ -159,15 +150,12 @@ class Telegram(RPC):
:param update: message update
:return: None
"""
# Fetch open trade
(err, df_statuses) = self.rpc_status_table()
if err:
self.send_msg(df_statuses, bot=bot)
else:
try:
df_statuses = self._rpc_status_table()
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
message = "<pre>{}</pre>".format(message)
self.send_msg(message, parse_mode=ParseMode.HTML)
self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _daily(self, bot: Bot, update: Update) -> None:
@@ -182,14 +170,12 @@ class Telegram(RPC):
timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError):
timescale = 7
(error, stats) = self.rpc_daily_profit(
timescale,
self._config['stake_currency'],
self._config['fiat_display_currency']
)
if error:
self.send_msg(stats, bot=bot)
else:
try:
stats = self._rpc_daily_profit(
timescale,
self._config['stake_currency'],
self._config['fiat_display_currency']
)
stats = tabulate(stats,
headers=[
'Day',
@@ -198,11 +184,10 @@ class Telegram(RPC):
],
tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
.format(
timescale,
stats
)
self.send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
.format(timescale, stats)
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _profit(self, bot: Bot, update: Update) -> None:
@@ -213,69 +198,63 @@ class Telegram(RPC):
:param update: message update
:return: None
"""
(error, stats) = self.rpc_trade_statistics(
self._config['stake_currency'],
self._config['fiat_display_currency']
)
if error:
self.send_msg(stats, bot=bot)
return
try:
stats = self._rpc_trade_statistics(
self._config['stake_currency'],
self._config['fiat_display_currency'])
# Message to display
markdown_msg = "*ROI:* Close trades\n" \
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
"*ROI:* All trades\n" \
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
"*Total Trade Count:* `{trade_count}`\n" \
"*First Trade opened:* `{first_trade_date}`\n" \
"*Latest Trade opened:* `{latest_trade_date}`\n" \
"*Avg. Duration:* `{avg_duration}`\n" \
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
.format(
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)
# Message to display
markdown_msg = "*ROI:* Close trades\n" \
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
"*ROI:* All trades\n" \
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \
"*Total Trade Count:* `{trade_count}`\n" \
"*First Trade opened:* `{first_trade_date}`\n" \
"*Latest Trade opened:* `{latest_trade_date}`\n" \
"*Avg. Duration:* `{avg_duration}`\n" \
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
.format(
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)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _balance(self, bot: Bot, update: Update) -> None:
"""
Handler for /balance
"""
(error, result) = self.rpc_balance(self._config['fiat_display_currency'])
if error:
self.send_msg('`All balances are zero.`')
return
""" Handler for /balance """
try:
currencys, total, symbol, value = \
self._rpc_balance(self._config['fiat_display_currency'])
output = ''
for currency in currencys:
output += "*{currency}:*\n" \
"\t`Available: {available: .8f}`\n" \
"\t`Balance: {balance: .8f}`\n" \
"\t`Pending: {pending: .8f}`\n" \
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
(currencys, total, symbol, value) = result
output = ''
for currency in currencys:
output += """*Currency*: {currency}
*Available*: {available}
*Balance*: {balance}
*Pending*: {pending}
*Est. BTC*: {est_btc: .8f}
""".format(**currency)
output += """*Estimated Value*:
*BTC*: {0: .8f}
*{1}*: {2: .2f}
""".format(total, symbol, value)
self.send_msg(output)
output += "\n*Estimated Value*:\n" \
"\t`BTC: {0: .8f}`\n" \
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
self._send_msg(output, bot=bot)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _start(self, bot: Bot, update: Update) -> None:
@@ -286,9 +265,8 @@ class Telegram(RPC):
:param update: message update
:return: None
"""
(error, msg) = self.rpc_start()
if error:
self.send_msg(msg, bot=bot)
msg = self._rpc_start()
self._send_msg(msg, bot=bot)
@authorized_only
def _stop(self, bot: Bot, update: Update) -> None:
@@ -299,8 +277,20 @@ class Telegram(RPC):
:param update: message update
:return: None
"""
(error, msg) = self.rpc_stop()
self.send_msg(msg, bot=bot)
msg = self._rpc_stop()
self._send_msg(msg, bot=bot)
@authorized_only
def _reload_conf(self, bot: Bot, update: Update) -> None:
"""
Handler for /reload_conf.
Triggers a config file reload
:param bot: telegram bot
:param update: message update
:return: None
"""
msg = self._rpc_reload_conf()
self._send_msg(msg, bot=bot)
@authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None:
@@ -313,10 +303,10 @@ class Telegram(RPC):
"""
trade_id = update.message.text.replace('/forcesell', '').strip()
(error, message) = self.rpc_forcesell(trade_id)
if error:
self.send_msg(message, bot=bot)
return
try:
self._rpc_forcesell(trade_id)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _performance(self, bot: Bot, update: Update) -> None:
@@ -327,19 +317,18 @@ class Telegram(RPC):
:param update: message update
:return: None
"""
(error, trades) = self.rpc_performance()
if error:
self.send_msg(trades, bot=bot)
return
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
index=i + 1,
pair=trade['pair'],
profit=trade['profit'],
count=trade['count']
) for i, trade in enumerate(trades))
message = '<b>Performance:</b>\n{}'.format(stats)
self.send_msg(message, parse_mode=ParseMode.HTML)
try:
trades = self._rpc_performance()
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
index=i + 1,
pair=trade['pair'],
profit=trade['profit'],
count=trade['count']
) for i, trade in enumerate(trades))
message = '<b>Performance:</b>\n{}'.format(stats)
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _count(self, bot: Bot, update: Update) -> None:
@@ -350,19 +339,18 @@ class Telegram(RPC):
:param update: message update
:return: None
"""
(error, trades) = self.rpc_count()
if error:
self.send_msg(trades, bot=bot)
return
message = tabulate({
'current': [len(trades)],
'max': [self._config['max_open_trades']],
'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
}, headers=['current', 'max', 'total stake'], tablefmt='simple')
message = "<pre>{}</pre>".format(message)
logger.debug(message)
self.send_msg(message, parse_mode=ParseMode.HTML)
try:
trades = self._rpc_count()
message = tabulate({
'current': [len(trades)],
'max': [self._config['max_open_trades']],
'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
}, headers=['current', 'max', 'total stake'], tablefmt='simple')
message = "<pre>{}</pre>".format(message)
logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only
def _help(self, bot: Bot, update: Update) -> None:
@@ -388,7 +376,7 @@ class Telegram(RPC):
"*/help:* `This help message`\n" \
"*/version:* `Show version`"
self.send_msg(message, bot=bot)
self._send_msg(message, bot=bot)
@authorized_only
def _version(self, bot: Bot, update: Update) -> None:
@@ -399,10 +387,10 @@ class Telegram(RPC):
:param update: message update
:return: None
"""
self.send_msg('*Version:* `{}`'.format(__version__), bot=bot)
self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def send_msg(self, msg: str, bot: Bot = None,
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
def _send_msg(self, msg: str, bot: Bot = None,
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message
:param msg: message
@@ -410,9 +398,6 @@ class Telegram(RPC):
:param parse_mode: telegram parse mode
:return: None
"""
if not self.is_enabled():
return
bot = bot or self._updater.bot
keyboard = [['/daily', '/profit', '/balance'],

View File

@@ -8,7 +8,8 @@ import enum
class State(enum.Enum):
"""
Bot running states
Bot application states
"""
RUNNING = 0
STOPPED = 1
RELOAD_CONF = 2

View File

@@ -0,0 +1,32 @@
import logging
from copy import deepcopy
from freqtrade.strategy.interface import IStrategy
logger = logging.getLogger(__name__)
def import_strategy(strategy: IStrategy) -> IStrategy:
"""
Imports given Strategy instance to global scope
of freqtrade.strategy and returns an instance of it
"""
# Copy all attributes from base class and class
attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__})
# Adjust module name
attr['__module__'] = 'freqtrade.strategy'
name = strategy.__class__.__name__
clazz = type(name, (IStrategy,), attr)
logger.debug(
'Imported strategy %s.%s as %s.%s',
strategy.__module__, strategy.__class__.__name__,
clazz.__module__, strategy.__class__.__name__,
)
# Modify global scope to declare class
globals()[name] = clazz
return clazz()

View File

@@ -26,7 +26,7 @@ class DefaultStrategy(IStrategy):
stoploss = -0.10
# Optimal ticker interval for the strategy
ticker_interval = 5
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""

View File

@@ -2,7 +2,7 @@
IStrategy interface
This module defines the interface to apply for strategies
"""
from typing import Dict
from abc import ABC, abstractmethod
from pandas import DataFrame
@@ -16,9 +16,13 @@ class IStrategy(ABC):
Attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> int: value of the ticker interval to use for the strategy
ticker_interval -> str: value of the ticker interval to use for the strategy
"""
minimal_roi: Dict
stoploss: float
ticker_interval: str
@abstractmethod
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""

View File

@@ -11,6 +11,7 @@ from collections import OrderedDict
from typing import Optional, Dict, Type
from freqtrade import constants
from freqtrade.strategy import import_strategy
from freqtrade.strategy.interface import IStrategy
@@ -33,7 +34,8 @@ class StrategyResolver(object):
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
self.strategy = self._load_strategy(strategy_name, extra_dir=config.get('strategy_path'))
self.strategy: IStrategy = self._load_strategy(strategy_name,
extra_dir=config.get('strategy_path'))
# Set attributes
# Check if we need to override configuration
@@ -59,10 +61,9 @@ class StrategyResolver(object):
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
key=lambda t: t[0]))
self.strategy.stoploss = float(self.strategy.stoploss)
self.strategy.ticker_interval = int(self.strategy.ticker_interval)
def _load_strategy(
self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]:
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
"""
Search and loads the specified strategy.
:param strategy_name: name of the module to import
@@ -71,7 +72,7 @@ class StrategyResolver(object):
"""
current_path = os.path.dirname(os.path.realpath(__file__))
abs_paths = [
os.path.join(current_path, '..', '..', 'user_data', 'strategies'),
os.path.join(os.getcwd(), 'user_data', 'strategies'),
current_path,
]
@@ -80,10 +81,13 @@ class StrategyResolver(object):
abs_paths.insert(0, extra_dir)
for path in abs_paths:
strategy = self._search_strategy(path, strategy_name)
if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
return strategy
try:
strategy = self._search_strategy(path, strategy_name)
if strategy:
logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path)
return import_strategy(strategy)
except FileNotFoundError:
logger.warning('Path "%s" does not exist', path)
raise ImportError(
"Impossible to load Strategy '{}'. This class does not exist"
@@ -100,9 +104,9 @@ class StrategyResolver(object):
"""
# Generate spec based on absolute path
spec = importlib.util.spec_from_file_location('user_data.strategies', module_path)
spec = importlib.util.spec_from_file_location('unknown', module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
valid_strategies_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass)

View File

@@ -2,17 +2,18 @@
import json
import logging
from datetime import datetime
from typing import Dict, Optional
from functools import reduce
from unittest.mock import MagicMock
import arrow
import pytest
from jsonschema import validate
from sqlalchemy import create_engine
from telegram import Chat, Message, Update
from freqtrade.analyze import Analyze
from freqtrade import constants
from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot
logging.getLogger('').setLevel(logging.INFO)
@@ -26,6 +27,20 @@ def log_has(line, logs):
False)
def patch_exchange(mocker, api_mock=None) -> None:
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
if api_mock:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
else:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
def get_patched_exchange(mocker, config, api_mock=None) -> Exchange:
patch_exchange(mocker, api_mock)
exchange = Exchange(config)
return exchange
# Functions for recurrent object patching
def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
"""
@@ -34,16 +49,38 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
:param config: Config to pass to the bot
:return: None
"""
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})
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
patch_exchange(mocker, None)
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
return FreqtradeBot(config, create_engine('sqlite://'))
return FreqtradeBot(config)
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
"""
Mocker to coinmarketcap to speed up tests
:param mocker: mocker to patch coinmarketcap class
:return: None
"""
tickermock = MagicMock(return_value={'price_usd': 12345.0})
listmock = MagicMock(return_value={'data': [{'id': 1, 'name': 'Bitcoin', 'symbol': 'BTC',
'website_slug': 'bitcoin'},
{'id': 1027, 'name': 'Ethereum', 'symbol': 'ETH',
'website_slug': 'ethereum'}
]})
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
ticker=tickermock,
listings=listmock,
)
@pytest.fixture(scope="function")
@@ -54,7 +91,7 @@ def default_conf():
"stake_currency": "BTC",
"stake_amount": 0.001,
"fiat_display_currency": "USD",
"ticker_interval": 5,
"ticker_interval": '5m',
"dry_run": True,
"minimal_roi": {
"40": 0.0,
@@ -73,11 +110,10 @@ def default_conf():
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_TKN",
"BTC_TRST",
"BTC_SWT",
"BTC_BCC"
"ETH/BTC",
"LTC/BTC",
"XRP/BTC",
"NEO/BTC"
]
},
"telegram": {
@@ -86,7 +122,8 @@ def default_conf():
"chat_id": "0"
},
"initial_state": "running",
"loglevel": logging.DEBUG
"db_url": "sqlite://",
"loglevel": logging.DEBUG,
}
validate(configuration, constants.CONF_SCHEMA)
return configuration
@@ -99,6 +136,11 @@ def update():
return _update
@pytest.fixture
def fee():
return MagicMock(return_value=0.0025)
@pytest.fixture
def ticker():
return MagicMock(return_value={
@@ -127,46 +169,178 @@ def ticker_sell_down():
@pytest.fixture
def health():
return MagicMock(return_value=[{
'Currency': 'BTC',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'ETH',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'TRST',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'SWT',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'BCC',
'IsActive': False,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}])
def markets():
return MagicMock(return_value=[
{
'id': 'ethbtc',
'symbol': 'ETH/BTC',
'base': 'ETH',
'quote': 'BTC',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'tknbtc',
'symbol': 'TKN/BTC',
'base': 'TKN',
'quote': 'BTC',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'blkbtc',
'symbol': 'BLK/BTC',
'base': 'BLK',
'quote': 'BTC',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'ltcbtc',
'symbol': 'LTC/BTC',
'base': 'LTC',
'quote': 'BTC',
'active': False,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'xrpbtc',
'symbol': 'XRP/BTC',
'base': 'XRP',
'quote': 'BTC',
'active': False,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
},
{
'id': 'neobtc',
'symbol': 'NEO/BTC',
'base': 'NEO',
'quote': 'BTC',
'active': False,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': {
'min': 1,
'max': 500000,
},
},
'info': '',
}
])
@pytest.fixture
def markets_empty():
return MagicMock(return_value=[])
@pytest.fixture(scope='function')
def limit_buy_order():
return {
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001099,
'datetime': arrow.utcnow().isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 0.0,
'closed': str(arrow.utcnow().datetime),
'status': 'closed'
}
@@ -174,12 +348,14 @@ def limit_buy_order():
def limit_buy_order_old():
return {
'id': 'mocked_limit_buy_old',
'type': 'LIMIT_BUY',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 90.99181073,
'status': 'open'
}
@@ -187,12 +363,14 @@ def limit_buy_order_old():
def limit_sell_order_old():
return {
'id': 'mocked_limit_sell_old',
'type': 'LIMIT_SELL',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'type': 'limit',
'side': 'sell',
'pair': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 90.99181073,
'status': 'open'
}
@@ -200,12 +378,14 @@ def limit_sell_order_old():
def limit_buy_order_old_partial():
return {
'id': 'mocked_limit_buy_old_partial',
'type': 'LIMIT_BUY',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'type': 'limit',
'side': 'buy',
'pair': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 67.99181073,
'status': 'open'
}
@@ -213,86 +393,228 @@ def limit_buy_order_old_partial():
def limit_sell_order():
return {
'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL',
'type': 'limit',
'side': 'sell',
'pair': 'mocked',
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001173,
'datetime': arrow.utcnow().isoformat(),
'price': 0.00001173,
'amount': 90.99181073,
'remaining': 0.0,
'closed': str(arrow.utcnow().datetime),
'status': 'closed'
}
@pytest.fixture
def ticker_history():
return [
{
"O": 8.794e-05,
"H": 8.948e-05,
"L": 8.794e-05,
"C": 8.88e-05,
"V": 991.09056638,
"T": "2017-11-26T08:50:00",
"BV": 0.0877869
},
{
"O": 8.88e-05,
"H": 8.942e-05,
"L": 8.88e-05,
"C": 8.893e-05,
"V": 658.77935965,
"T": "2017-11-26T08:55:00",
"BV": 0.05874751
},
{
"O": 8.891e-05,
"H": 8.893e-05,
"L": 8.875e-05,
"C": 8.877e-05,
"V": 7920.73570705,
"T": "2017-11-26T09:00:00",
"BV": 0.7039405
}
[
1511686200000, # unix timestamp ms
8.794e-05, # open
8.948e-05, # high
8.794e-05, # low
8.88e-05, # close
0.0877869, # volume (in quote currency)
],
[
1511686500000,
8.88e-05,
8.942e-05,
8.88e-05,
8.893e-05,
0.05874751,
],
[
1511686800000,
8.891e-05,
8.893e-05,
8.875e-05,
8.877e-05,
0.7039405
]
]
@pytest.fixture
def ticker_history_without_bv():
return [
{
"O": 8.794e-05,
"H": 8.948e-05,
"L": 8.794e-05,
"C": 8.88e-05,
"V": 991.09056638,
"T": "2017-11-26T08:50:00"
def tickers():
return MagicMock(return_value={
'ETH/BTC': {
'symbol': 'ETH/BTC',
'timestamp': 1522014806207,
'datetime': '2018-03-25T21:53:26.207Z',
'high': 0.061697,
'low': 0.060531,
'bid': 0.061588,
'bidVolume': 3.321,
'ask': 0.061655,
'askVolume': 0.212,
'vwap': 0.06105296,
'open': 0.060809,
'close': 0.060761,
'first': None,
'last': 0.061588,
'change': 1.281,
'percentage': None,
'average': None,
'baseVolume': 111649.001,
'quoteVolume': 6816.50176926,
'info': {}
},
{
"O": 8.88e-05,
"H": 8.942e-05,
"L": 8.88e-05,
"C": 8.893e-05,
"V": 658.77935965,
"T": "2017-11-26T08:55:00"
'TKN/BTC': {
'symbol': 'TKN/BTC',
'timestamp': 1522014806169,
'datetime': '2018-03-25T21:53:26.169Z',
'high': 0.01885,
'low': 0.018497,
'bid': 0.018799,
'bidVolume': 8.38,
'ask': 0.018802,
'askVolume': 15.0,
'vwap': 0.01869197,
'open': 0.018585,
'close': 0.018573,
'baseVolume': 81058.66,
'quoteVolume': 2247.48374509,
},
{
"O": 8.891e-05,
"H": 8.893e-05,
"L": 8.875e-05,
"C": 8.877e-05,
"V": 7920.73570705,
"T": "2017-11-26T09:00:00"
'BLK/BTC': {
'symbol': 'BLK/BTC',
'timestamp': 1522014806072,
'datetime': '2018-03-25T21:53:26.720Z',
'high': 0.007745,
'low': 0.007512,
'bid': 0.007729,
'bidVolume': 0.01,
'ask': 0.007743,
'askVolume': 21.37,
'vwap': 0.00761466,
'open': 0.007653,
'close': 0.007652,
'first': None,
'last': 0.007743,
'change': 1.176,
'percentage': None,
'average': None,
'baseVolume': 295152.26,
'quoteVolume': 1515.14631229,
'info': {}
},
'LTC/BTC': {
'symbol': 'LTC/BTC',
'timestamp': 1523787258992,
'datetime': '2018-04-15T10:14:19.992Z',
'high': 0.015978,
'low': 0.0157,
'bid': 0.015954,
'bidVolume': 12.83,
'ask': 0.015957,
'askVolume': 0.49,
'vwap': 0.01581636,
'open': 0.015823,
'close': 0.01582,
'first': None,
'last': 0.015951,
'change': 0.809,
'percentage': None,
'average': None,
'baseVolume': 88620.68,
'quoteVolume': 1401.65697943,
'info': {}
},
'ETH/USDT': {
'symbol': 'ETH/USDT',
'timestamp': 1522014804118,
'datetime': '2018-03-25T21:53:24.118Z',
'high': 530.88,
'low': 512.0,
'bid': 529.73,
'bidVolume': 0.2,
'ask': 530.21,
'askVolume': 0.2464,
'vwap': 521.02438405,
'open': 527.27,
'close': 528.42,
'first': None,
'last': 530.21,
'change': 0.558,
'percentage': None,
'average': None,
'baseVolume': 72300.0659,
'quoteVolume': 37670097.3022171,
'info': {}
},
'TKN/USDT': {
'symbol': 'TKN/USDT',
'timestamp': 1522014806198,
'datetime': '2018-03-25T21:53:26.198Z',
'high': 8718.0,
'low': 8365.77,
'bid': 8603.64,
'bidVolume': 0.15846,
'ask': 8603.67,
'askVolume': 0.069147,
'vwap': 8536.35621697,
'open': 8680.0,
'close': 8680.0,
'first': None,
'last': 8603.67,
'change': -0.879,
'percentage': None,
'average': None,
'baseVolume': 30414.604298,
'quoteVolume': 259629896.48584127,
'info': {}
},
'BLK/USDT': {
'symbol': 'BLK/USDT',
'timestamp': 1522014806145,
'datetime': '2018-03-25T21:53:26.145Z',
'high': 66.95,
'low': 63.38,
'bid': 66.473,
'bidVolume': 4.968,
'ask': 66.54,
'askVolume': 2.704,
'vwap': 65.0526901,
'open': 66.43,
'close': 66.383,
'first': None,
'last': 66.5,
'change': 0.105,
'percentage': None,
'average': None,
'baseVolume': 294106.204,
'quoteVolume': 19132399.743954,
'info': {}
},
'LTC/USDT': {
'symbol': 'LTC/USDT',
'timestamp': 1523787257812,
'datetime': '2018-04-15T10:14:18.812Z',
'high': 129.94,
'low': 124.0,
'bid': 129.28,
'bidVolume': 0.03201,
'ask': 129.52,
'askVolume': 0.14529,
'vwap': 126.92838682,
'open': 127.0,
'close': 127.1,
'first': None,
'last': 129.28,
'change': 1.795,
'percentage': None,
'average': None,
'baseVolume': 59698.79897,
'quoteVolume': 29132399.743954,
'info': {}
}
]
})
# FIX: Perhaps change result fixture to use BTC_UNITEST instead?
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/BTC_ETH-1.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))
# FIX:
# Create an fixture/function
# that inserts a trade of some type and open-status
@@ -300,132 +622,88 @@ def result():
# See tests in rpc/main that could use this
@pytest.fixture(scope="function")
def trades_for_order():
return [{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
'id': '34567',
'order': '123456',
'type': None,
'side': 'buy',
'price': 0.245441,
'cost': 1.963528,
'amount': 8.0,
'fee': {'cost': 0.008, 'currency': 'LTC'}}]
@pytest.fixture(scope="function")
def trades_for_order2():
return [{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
'id': '34567',
'order': '123456',
'type': None,
'side': 'buy',
'price': 0.245441,
'cost': 1.963528,
'amount': 4.0,
'fee': {'cost': 0.004, 'currency': 'LTC'}},
{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
'id': '34567',
'order': '123456',
'type': None,
'side': 'buy',
'price': 0.245441,
'cost': 1.963528,
'amount': 4.0,
'fee': {'cost': 0.004, 'currency': 'LTC'}}]
@pytest.fixture
def get_market_summaries_data():
"""
This fixture is a real result from exchange.get_market_summaries() but reduced to only
8 entries. 4 BTC, 4 USTD
:return: JSON market summaries
"""
return [
{
'Ask': 1.316e-05,
'BaseVolume': 5.72599471,
'Bid': 1.3e-05,
'Created': '2014-04-14T00:00:00',
'High': 1.414e-05,
'Last': 1.298e-05,
'Low': 1.282e-05,
'MarketName': 'BTC-XWC',
'OpenBuyOrders': 2000,
'OpenSellOrders': 1484,
'PrevDay': 1.376e-05,
'TimeStamp': '2018-02-05T01:32:40.493',
'Volume': 424041.21418375
},
{
'Ask': 0.00627051,
'BaseVolume': 93.23302388,
'Bid': 0.00618192,
'Created': '2016-10-20T04:48:30.387',
'High': 0.00669897,
'Last': 0.00618192,
'Low': 0.006,
'MarketName': 'BTC-XZC',
'OpenBuyOrders': 343,
'OpenSellOrders': 2037,
'PrevDay': 0.00668229,
'TimeStamp': '2018-02-05T01:32:43.383',
'Volume': 14863.60730702
},
{
'Ask': 0.01137247,
'BaseVolume': 383.55922657,
'Bid': 0.01136006,
'Created': '2016-11-15T20:29:59.73',
'High': 0.012,
'Last': 0.01137247,
'Low': 0.01119883,
'MarketName': 'BTC-ZCL',
'OpenBuyOrders': 1332,
'OpenSellOrders': 5317,
'PrevDay': 0.01179603,
'TimeStamp': '2018-02-05T01:32:42.773',
'Volume': 33308.07358285
},
{
'Ask': 0.04155821,
'BaseVolume': 274.75369074,
'Bid': 0.04130002,
'Created': '2016-10-28T17:13:10.833',
'High': 0.04354429,
'Last': 0.041585,
'Low': 0.0413,
'MarketName': 'BTC-ZEC',
'OpenBuyOrders': 863,
'OpenSellOrders': 5579,
'PrevDay': 0.0429,
'TimeStamp': '2018-02-05T01:32:43.21',
'Volume': 6479.84033259
},
{
'Ask': 210.99999999,
'BaseVolume': 615132.70989532,
'Bid': 210.05503736,
'Created': '2017-07-21T01:08:49.397',
'High': 257.396,
'Last': 211.0,
'Low': 209.05333589,
'MarketName': 'USDT-XMR',
'OpenBuyOrders': 180,
'OpenSellOrders': 1203,
'PrevDay': 247.93528899,
'TimeStamp': '2018-02-05T01:32:43.117',
'Volume': 2688.17410793
},
{
'Ask': 0.79589979,
'BaseVolume': 9349557.01853031,
'Bid': 0.789226,
'Created': '2017-07-14T17:10:10.737',
'High': 0.977,
'Last': 0.79589979,
'Low': 0.781,
'MarketName': 'USDT-XRP',
'OpenBuyOrders': 1075,
'OpenSellOrders': 6508,
'PrevDay': 0.93300218,
'TimeStamp': '2018-02-05T01:32:42.383',
'Volume': 10801663.00788851
},
{
'Ask': 0.05154982,
'BaseVolume': 2311087.71232136,
'Bid': 0.05040107,
'Created': '2017-12-29T19:29:18.357',
'High': 0.06668561,
'Last': 0.0508,
'Low': 0.05006731,
'MarketName': 'USDT-XVG',
'OpenBuyOrders': 655,
'OpenSellOrders': 5544,
'PrevDay': 0.0627,
'TimeStamp': '2018-02-05T01:32:41.507',
'Volume': 40031424.2152716
},
{
'Ask': 332.65500022,
'BaseVolume': 562911.87455665,
'Bid': 330.00000001,
'Created': '2017-07-14T17:10:10.673',
'High': 401.59999999,
'Last': 332.65500019,
'Low': 330.0,
'MarketName': 'USDT-ZEC',
'OpenBuyOrders': 161,
'OpenSellOrders': 1731,
'PrevDay': 391.42,
'TimeStamp': '2018-02-05T01:32:42.947',
'Volume': 1571.09647946
}
]
def buy_order_fee():
return {
'id': 'mocked_limit_buy_old',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.245441,
'amount': 8.0,
'remaining': 90.99181073,
'status': 'closed',
'fee': None
}

View File

@@ -1,33 +1,22 @@
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
# pragma pylint: disable=protected-access
import logging
from copy import deepcopy
from random import randint
from unittest.mock import MagicMock
from datetime import datetime
from unittest.mock import MagicMock, PropertyMock
import ccxt
import pytest
from requests.exceptions import RequestException
import freqtrade.exchange as exchange
from freqtrade import OperationalException
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
get_ticker, get_ticker_history, cancel_order, get_name, get_fee
from freqtrade.tests.conftest import log_has
API_INIT = False
def maybe_init_api(conf, mocker, force=False):
global API_INIT
if force or not API_INIT:
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
init(config=conf)
API_INIT = True
from freqtrade import OperationalException, DependencyException, TemporaryError
from freqtrade.exchange import Exchange, API_RETRY_COUNT
from freqtrade.tests.conftest import log_has, get_patched_exchange
def test_init(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
maybe_init_api(default_conf, mocker, True)
get_patched_exchange(mocker, default_conf)
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
@@ -37,250 +26,706 @@ def test_init_exception(default_conf):
with pytest.raises(
OperationalException,
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
init(config=default_conf)
Exchange(default_conf)
def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[
'BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT', 'BTC_BCC',
])
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
validate_pairs(default_conf['exchange']['pair_whitelist'])
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
Exchange(default_conf)
def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[])
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock.load_markets = MagicMock(return_value={})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
with pytest.raises(OperationalException, match=r'not available'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
Exchange(default_conf)
def test_validate_pairs_not_compatible(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(
return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT'])
default_conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
})
conf = deepcopy(default_conf)
conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
with pytest.raises(OperationalException, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
Exchange(conf)
def test_validate_pairs_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
api_mock.get_markets = MagicMock(side_effect=RequestException())
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance'))
# with pytest.raises(RequestException, match=r'Unable to validate pairs'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
api_mock.load_markets = MagicMock(return_value={})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
Exchange(default_conf)
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 validate pairs (assuming they are correct). Reason: ',
caplog.record_tuples)
def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
conf = deepcopy(default_conf)
conf['stake_currency'] = 'ETH'
api_mock = MagicMock()
api_mock.name = MagicMock(return_value='binance')
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
with pytest.raises(
OperationalException,
match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
):
Exchange(conf)
def test_buy_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
exchange = get_patched_exchange(mocker, default_conf)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
order = exchange.buy(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'dry_run_buy_' in order['id']
def test_buy_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.buy = MagicMock(
return_value='dry_run_buy_{}'.format(randint(0, 10**6)))
mocker.patch('freqtrade.exchange._API', api_mock)
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.create_limit_buy_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
order = exchange.buy(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InsufficientFunds)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(DependencyException):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(TemporaryError):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(OperationalException):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.buy(pair='ETH/BTC', rate=200, amount=1)
def test_sell_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
exchange = get_patched_exchange(mocker, default_conf)
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
order = exchange.sell(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'dry_run_sell_' in order['id']
def test_sell_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.sell = MagicMock(
return_value='dry_run_sell_{}'.format(randint(0, 10**6)))
mocker.patch('freqtrade.exchange._API', api_mock)
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
api_mock.create_limit_sell_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
order = exchange.sell(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InsufficientFunds)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(DependencyException):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(TemporaryError):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(OperationalException):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.sell(pair='ETH/BTC', rate=200, amount=1)
def test_get_balance_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balance(currency='BTC') == 999.9
exchange = get_patched_exchange(mocker, default_conf)
assert exchange.get_balance(currency='BTC') == 999.9
def test_get_balance_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_balance = MagicMock(return_value=123.4)
mocker.patch('freqtrade.exchange._API', api_mock)
api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4}})
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balance(currency='BTC') == 123.4
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_balance(currency='BTC') == 123.4
with pytest.raises(OperationalException):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_balance(currency='BTC')
def test_get_balances_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balances() == []
exchange = get_patched_exchange(mocker, default_conf)
assert exchange.get_balances() == {}
def test_get_balances_prod(default_conf, mocker):
balance_item = {
'Currency': '1ST',
'Balance': 10.0,
'Available': 10.0,
'Pending': 0.0,
'CryptoAddress': None
'free': 10.0,
'total': 10.0,
'used': 0.0
}
api_mock = MagicMock()
api_mock.get_balances = MagicMock(
return_value=[balance_item, balance_item, balance_item])
mocker.patch('freqtrade.exchange._API', api_mock)
api_mock.fetch_balance = MagicMock(return_value={
'1ST': balance_item,
'2ST': balance_item,
'3ST': balance_item
})
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert len(exchange.get_balances()) == 3
assert exchange.get_balances()['1ST']['free'] == 10.0
assert exchange.get_balances()['1ST']['total'] == 10.0
assert exchange.get_balances()['1ST']['used'] == 0.0
assert len(get_balances()) == 3
assert get_balances()[0]['Currency'] == '1ST'
assert get_balances()[0]['Balance'] == 10.0
assert get_balances()[0]['Available'] == 10.0
assert get_balances()[0]['Pending'] == 0.0
with pytest.raises(TemporaryError):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError)
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
# This test is somewhat redundant with
# test_exchange_bittrex.py::test_exchange_bittrex_get_ticker
def test_get_ticker(default_conf, mocker):
maybe_init_api(default_conf, mocker)
def test_get_tickers(default_conf, mocker):
api_mock = MagicMock()
tick = {"success": True, 'result': {'Bid': 0.00001098, 'Ask': 0.00001099, 'Last': 0.0001}}
api_mock.get_ticker = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
tick = {'ETH/BTC': {
'symbol': 'ETH/BTC',
'bid': 0.5,
'ask': 1,
'last': 42,
}, 'BCH/BTC': {
'symbol': 'BCH/BTC',
'bid': 0.6,
'ask': 0.5,
'last': 41,
}
}
api_mock.fetch_tickers = MagicMock(return_value=tick)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# retrieve original ticker
ticker = get_ticker(pair='BTC_ETH')
tickers = exchange.get_tickers()
assert 'ETH/BTC' in tickers
assert 'BCH/BTC' in tickers
assert tickers['ETH/BTC']['bid'] == 0.5
assert tickers['ETH/BTC']['ask'] == 1
assert tickers['BCH/BTC']['bid'] == 0.6
assert tickers['BCH/BTC']['ask'] == 0.5
with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NetworkError)
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):
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_tickers()
api_mock.fetch_tickers = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_tickers()
def test_get_ticker(default_conf, mocker):
api_mock = MagicMock()
tick = {
'symbol': 'ETH/BTC',
'bid': 0.00001098,
'ask': 0.00001099,
'last': 0.0001,
}
api_mock.fetch_ticker = MagicMock(return_value=tick)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# retrieve original ticker
ticker = exchange.get_ticker(pair='ETH/BTC')
assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099
# change the ticker
tick = {"success": True, 'result': {"Bid": 0.5, "Ask": 1, "Last": 42}}
api_mock.get_ticker = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
tick = {
'symbol': 'ETH/BTC',
'bid': 0.5,
'ask': 1,
'last': 42,
}
api_mock.fetch_ticker = MagicMock(return_value=tick)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# if not caching the result we should get the same ticker
# if not fetching a new result we should get the cached ticker
ticker = get_ticker(pair='BTC_ETH', refresh=False)
assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099
ticker = exchange.get_ticker(pair='ETH/BTC')
# force ticker refresh
ticker = get_ticker(pair='BTC_ETH', refresh=True)
assert api_mock.fetch_ticker.call_count == 1
assert ticker['bid'] == 0.5
assert ticker['ask'] == 1
assert 'ETH/BTC' in exchange._cached_ticker
assert exchange._cached_ticker['ETH/BTC']['bid'] == 0.5
assert exchange._cached_ticker['ETH/BTC']['ask'] == 1
# Test caching
api_mock.fetch_ticker = MagicMock()
exchange.get_ticker(pair='ETH/BTC', refresh=False)
assert api_mock.fetch_ticker.call_count == 0
with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
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={})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_ticker(pair='ETH/BTC', refresh=True)
def make_fetch_ohlcv_mock(data):
def fetch_ohlcv_mock(pair, timeframe, since):
if since:
assert since > data[-1][0]
return []
return data
return fetch_ohlcv_mock
def test_get_ticker_history(default_conf, mocker):
api_mock = MagicMock()
tick = 123
api_mock.get_ticker_history = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange._API', api_mock)
tick = [
[
1511686200000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# retrieve original ticker
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
assert ticks == 123
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686200000
assert ticks[0][1] == 1
assert ticks[0][2] == 2
assert ticks[0][3] == 3
assert ticks[0][4] == 4
assert ticks[0][5] == 5
# change the ticker
tick = 999
api_mock.get_ticker_history = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange._API', api_mock)
# change ticker and ensure tick changes
new_tick = [
[
1511686210000, # unix timestamp ms
6, # open
7, # high
8, # low
9, # close
10, # volume (in quote currency)
]
]
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# ensure caching will still return the original ticker
ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval']))
assert ticks == 123
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686210000
assert ticks[0][1] == 6
assert ticks[0][2] == 7
assert ticks[0][3] == 8
assert ticks[0][4] == 9
assert ticks[0][5] == 10
with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# new symbol to get around cache
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):
api_mock = MagicMock()
# GDAX use-case (real data from GDAX)
# This ticker history is ordered DESC (newest first, oldest last)
tick = [
[1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264],
[1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526],
[1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001],
[1527832200000, 0.07658, 0.07658, 0.07655, 0.07656, 0.59780186],
[1527831900000, 0.07658, 0.07658, 0.07658, 0.07658, 1.76278136],
[1527831600000, 0.07658, 0.07658, 0.07658, 0.07658, 2.22646521],
[1527831300000, 0.07655, 0.07657, 0.07655, 0.07657, 1.1753],
[1527831000000, 0.07654, 0.07654, 0.07651, 0.07651, 0.8073060299999999],
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
]
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527830400000
assert ticks[0][1] == 0.07649
assert ticks[0][2] == 0.07651
assert ticks[0][3] == 0.07649
assert ticks[0][4] == 0.07651
assert ticks[0][5] == 2.5734867
assert ticks[9][0] == 1527833100000
assert ticks[9][1] == 0.07666
assert ticks[9][2] == 0.07671
assert ticks[9][3] == 0.07666
assert ticks[9][4] == 0.07668
assert ticks[9][5] == 16.65244264
# Bittrex use-case (real data from Bittrex)
# This ticker history is ordered ASC (oldest first, newest last)
tick = [
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
[1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037],
[1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124],
[1527828600000, 0.0764, 0.0766, 0.0764, 0.0766, 5.71044773],
[1527828900000, 0.0764, 0.07666998, 0.0764, 0.07666998, 47.48888565],
[1527829200000, 0.0765, 0.07672999, 0.0765, 0.07672999, 3.37640326],
[1527829500000, 0.0766, 0.07675, 0.0765, 0.07675, 8.36203831],
[1527829800000, 0.07675, 0.07677999, 0.07620002, 0.076695, 119.22963884],
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
]
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
# Test the ticker history sort
ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527827700000
assert ticks[0][1] == 0.07659999
assert ticks[0][2] == 0.0766
assert ticks[0][3] == 0.07627
assert ticks[0][4] == 0.07657998
assert ticks[0][5] == 1.85216924
assert ticks[9][0] == 1527830400000
assert ticks[9][1] == 0.07671
assert ticks[9][2] == 0.07674399
assert ticks[9][3] == 0.07629216
assert ticks[9][4] == 0.07655213
assert ticks[9][5] == 2.31452783
def test_cancel_order_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert cancel_order(order_id='123') is None
exchange = get_patched_exchange(mocker, default_conf)
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') is None
# Ensure that if not dry_run, we should call API
def test_cancel_order(default_conf, mocker):
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123)
mocker.patch('freqtrade.exchange._API', api_mock)
assert cancel_order(order_id='_') == 123
exchange = get_patched_exchange(mocker, default_conf, api_mock)
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):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
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(OperationalException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == 1
def test_get_order(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
order = MagicMock()
order.myid = 123
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
print(exchange.get_order('X'))
assert exchange.get_order('X').myid == 123
exchange = get_patched_exchange(mocker, default_conf)
exchange._dry_run_open_orders['X'] = order
print(exchange.get_order('X', 'TKN/BTC'))
assert exchange.get_order('X', 'TKN/BTC').myid == 123
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock = MagicMock()
api_mock.get_order = MagicMock(return_value=456)
mocker.patch('freqtrade.exchange._API', api_mock)
assert exchange.get_order('X') == 456
api_mock.fetch_order = MagicMock(return_value=456)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
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):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
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(OperationalException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1
def test_get_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs',
def test_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'bittrex'
init(default_conf)
default_conf['exchange']['name'] = 'binance'
exchange = Exchange(default_conf)
assert get_name() == 'Bittrex'
assert exchange.name == 'Binance'
def test_id(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance'
exchange = Exchange(default_conf)
assert exchange.id == 'binance'
def test_get_pair_detail_url(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance'
exchange = Exchange(default_conf)
url = exchange.get_pair_detail_url('TKN/ETH')
assert 'TKN' in url
assert 'ETH' in url
url = exchange.get_pair_detail_url('LOOONG/BTC')
assert 'LOOONG' in url
assert 'BTC' in url
default_conf['exchange']['name'] = 'bittrex'
exchange = Exchange(default_conf)
url = exchange.get_pair_detail_url('TKN/ETH')
assert 'TKN' in url
assert 'ETH' in url
url = exchange.get_pair_detail_url('LOOONG/BTC')
assert 'LOOONG' in url
assert 'BTC' in url
default_conf['exchange']['name'] = 'poloniex'
exchange = Exchange(default_conf)
url = exchange.get_pair_detail_url('LOOONG/BTC')
assert '' == url
assert log_has('Could not get exchange url for Poloniex', caplog.record_tuples)
def test_get_trades_for_order(default_conf, mocker):
order_id = 'ABCD-ABCD'
since = datetime(2018, 5, 5)
default_conf["dry_run"] = False
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
api_mock = MagicMock()
api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV',
'order': 'ABCD-ABCD',
'info': {'pair': 'XLTCZBTC',
'time': 1519860024.4388,
'type': 'buy',
'ordertype': 'limit',
'price': '20.00000',
'cost': '38.62000',
'fee': '0.06179',
'vol': '5',
'id': 'ABCD-ABCD'},
'timestamp': 1519860024438,
'datetime': '2018-02-28T23:20:24.438Z',
'symbol': 'LTC/BTC',
'type': 'limit',
'side': 'buy',
'price': 165.0,
'amount': 0.2340606,
'fee': {'cost': 0.06179, 'currency': 'BTC'}
}])
exchange = get_patched_exchange(mocker, default_conf, api_mock)
orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
assert len(orders) == 1
assert orders[0]['price'] == 165
# test Exceptions
with pytest.raises(OperationalException):
api_mock = MagicMock()
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):
api_mock = MagicMock()
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):
api_mock = MagicMock()
api_mock.fetch_markets = markets
exchange = get_patched_exchange(mocker, default_conf, api_mock)
ret = exchange.get_markets()
assert isinstance(ret, list)
assert len(ret) == 6
assert ret[0]["id"] == "ethbtc"
assert ret[0]["symbol"] == "ETH/BTC"
# test Exceptions
with pytest.raises(OperationalException):
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):
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
init(default_conf)
assert get_fee() == 0.0025
def test_exchange_misc(mocker):
api_mock = MagicMock()
mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_markets()
assert api_mock.get_markets.call_count == 1
exchange.get_market_summaries()
assert api_mock.get_market_summaries.call_count == 1
api_mock.name = 123
assert exchange.get_name() == 123
api_mock.fee = 456
assert exchange.get_fee() == 456
exchange.get_wallet_health()
assert api_mock.get_wallet_health.call_count == 1
api_mock.calculate_fee = MagicMock(return_value={
'type': 'taker',
'currency': 'BTC',
'rate': 0.025,
'cost': 0.05
})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_fee() == 0.025
# test Exceptions
with pytest.raises(OperationalException):
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

@@ -1,349 +0,0 @@
# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument
from unittest.mock import MagicMock
import pytest
from requests.exceptions import ContentDecodingError
import freqtrade.exchange.bittrex as btx
from freqtrade.exchange.bittrex import Bittrex
# Eat this flake8
# +------------------+
# | bittrex.Bittrex |
# +------------------+
# |
# (mock Fake_bittrex)
# |
# +-----------------------------+
# | freqtrade.exchange.Bittrex |
# +-----------------------------+
# Call into Bittrex will flow up to the
# external package bittrex.Bittrex.
# By inserting a mock, we redirect those
# calls.
# The faked bittrex API is called just 'fb'
# The freqtrade.exchange.Bittrex is a
# wrapper, and is called 'wb'
def _stub_config():
return {'key': '',
'secret': ''}
class FakeBittrex():
def __init__(self, success=True):
self.success = True # Believe in yourself
self.result = None
self.get_ticker_call_count = 0
# This is really ugly, doing side-effect during instance creation
# But we're allowed to in testing-code
btx._API = MagicMock()
btx._API.buy_limit = self.fake_buysell_limit
btx._API.sell_limit = self.fake_buysell_limit
btx._API.get_balance = self.fake_get_balance
btx._API.get_balances = self.fake_get_balances
btx._API.get_ticker = self.fake_get_ticker
btx._API.get_order = self.fake_get_order
btx._API.cancel = self.fake_cancel_order
btx._API.get_markets = self.fake_get_markets
btx._API.get_market_summaries = self.fake_get_market_summaries
btx._API_V2 = MagicMock()
btx._API_V2.get_candles = self.fake_get_candles
btx._API_V2.get_wallet_health = self.fake_get_wallet_health
def fake_buysell_limit(self, pair, amount, limit):
return {'success': self.success,
'result': {'uuid': '1234'},
'message': 'barter'}
def fake_get_balance(self, cur):
return {'success': self.success,
'result': {'Balance': 1234},
'message': 'unbalanced'}
def fake_get_balances(self):
return {'success': self.success,
'result': [{'BTC_ETH': 1234}],
'message': 'no balances'}
def fake_get_ticker(self, pair):
self.get_ticker_call_count += 1
return self.result or {'success': self.success,
'result': {'Bid': 1, 'Ask': 1, 'Last': 1},
'message': 'NO_API_RESPONSE'}
def fake_get_candles(self, pair, interval):
return self.result or {'success': self.success,
'result': [{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}],
'message': 'candles lit'}
def fake_get_order(self, uuid):
return {'success': self.success,
'result': {'OrderUuid': 'ABC123',
'Type': 'Type',
'Exchange': 'BTC_ETH',
'Opened': True,
'PricePerUnit': 1,
'Quantity': 1,
'QuantityRemaining': 1,
'Closed': True},
'message': 'lost'}
def fake_cancel_order(self, uuid):
return self.result or {'success': self.success,
'message': 'no such order'}
def fake_get_markets(self):
return self.result or {'success': self.success,
'message': 'market gone',
'result': [{'MarketName': '-_'}]}
def fake_get_market_summaries(self):
return self.result or {'success': self.success,
'message': 'no summary',
'result': ['sum']}
def fake_get_wallet_health(self):
return self.result or {'success': self.success,
'message': 'bad health',
'result': [{'Health': {'Currency': 'BTC_ETH',
'IsActive': True,
'LastChecked': 0},
'Currency': {'Notice': True}}]}
# The freqtrade.exchange.bittrex is called wrap_bittrex
# to not confuse naming with bittrex.bittrex
def make_wrap_bittrex():
conf = _stub_config()
wb = btx.Bittrex(conf)
return wb
def test_exchange_bittrex_class():
conf = _stub_config()
b = Bittrex(conf)
assert isinstance(b, Bittrex)
slots = dir(b)
for name in ['fee', 'buy', 'sell', 'get_balance', 'get_balances',
'get_ticker', 'get_ticker_history', 'get_order',
'cancel_order', 'get_pair_detail_url', 'get_markets',
'get_market_summaries', 'get_wallet_health']:
assert name in slots
# FIX: ensure that the slot is also a method in the class
# getattr(b, name) => bound method Bittrex.buy
# type(getattr(b, name)) => class 'method'
def test_exchange_bittrex_fee():
fee = Bittrex.fee.__get__(Bittrex)
assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
def test_exchange_bittrex_buy_good():
wb = make_wrap_bittrex()
fb = FakeBittrex()
uuid = wb.buy('BTC_ETH', 1, 1)
assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'barter.*'):
wb.buy('BAD', 1, 1)
def test_exchange_bittrex_sell_good():
wb = make_wrap_bittrex()
fb = FakeBittrex()
uuid = wb.sell('BTC_ETH', 1, 1)
assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'barter.*'):
uuid = wb.sell('BAD', 1, 1)
def test_exchange_bittrex_get_balance():
wb = make_wrap_bittrex()
fb = FakeBittrex()
bal = wb.get_balance('BTC_ETH')
assert bal == fb.fake_get_balance(1)['result']['Balance']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'unbalanced'):
wb.get_balance('BTC_ETH')
def test_exchange_bittrex_get_balances():
wb = make_wrap_bittrex()
fb = FakeBittrex()
bals = wb.get_balances()
assert bals == fb.fake_get_balances()['result']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'no balances'):
wb.get_balances()
def test_exchange_bittrex_get_ticker():
wb = make_wrap_bittrex()
fb = FakeBittrex()
# Poll ticker, which updates the cache
tick = wb.get_ticker('BTC_ETH')
for x in ['bid', 'ask', 'last']:
assert x in tick
# Ensure the side-effect was made (update the ticker cache)
assert 'BTC_ETH' in wb.cached_ticker.keys()
# taint the cache, so we can recognize the cache wall utilized
wb.cached_ticker['BTC_ETH']['bid'] = 1234
# Poll again, getting the cached result
fb.get_ticker_call_count = 0
tick = wb.get_ticker('BTC_ETH', False)
# Ensure the result was from the cache, and that we didn't call exchange
assert wb.cached_ticker['BTC_ETH']['bid'] == 1234
assert fb.get_ticker_call_count == 0
def test_exchange_bittrex_get_ticker_bad():
wb = make_wrap_bittrex()
fb = FakeBittrex()
fb.result = {'success': True, 'result': {'Bid': 1, 'Ask': 0}} # incomplete result
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
wb.get_ticker('BTC_ETH')
fb.result = {'success': False, 'message': 'gone bad'}
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
wb.get_ticker('BTC_ETH')
fb.result = {'success': True, 'result': {}} # incomplete result
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
wb.get_ticker('BTC_ETH')
fb.result = {'success': False, 'message': 'gone bad'}
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
wb.get_ticker('BTC_ETH')
fb.result = {'success': True,
'result': {'Bid': 1, 'Ask': 0, 'Last': None}} # incomplete result
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
wb.get_ticker('BTC_ETH')
def test_exchange_bittrex_get_ticker_history_intervals():
wb = make_wrap_bittrex()
FakeBittrex()
for tick_interval in [1, 5, 30, 60, 1440]:
assert ([{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}] ==
wb.get_ticker_history('BTC_ETH', tick_interval))
def test_exchange_bittrex_get_ticker_history():
wb = make_wrap_bittrex()
fb = FakeBittrex()
assert wb.get_ticker_history('BTC_ETH', 5)
with pytest.raises(ValueError, match=r'.*Unknown tick_interval.*'):
wb.get_ticker_history('BTC_ETH', 2)
fb.success = False
with pytest.raises(btx.OperationalException, match=r'candles lit.*'):
wb.get_ticker_history('BTC_ETH', 5)
fb.success = True
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex.*'):
fb.result = {'bad': 0}
wb.get_ticker_history('BTC_ETH', 5)
with pytest.raises(ContentDecodingError, match=r'.*Required property C not present.*'):
fb.result = {'success': True,
'result': [{'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}], # close is missing
'message': 'candles lit'}
wb.get_ticker_history('BTC_ETH', 5)
def test_exchange_bittrex_get_order():
wb = make_wrap_bittrex()
fb = FakeBittrex()
order = wb.get_order('someUUID')
assert order['id'] == 'ABC123'
fb.success = False
with pytest.raises(btx.OperationalException, match=r'lost'):
wb.get_order('someUUID')
def test_exchange_bittrex_cancel_order():
wb = make_wrap_bittrex()
fb = FakeBittrex()
wb.cancel_order('someUUID')
with pytest.raises(btx.OperationalException, match=r'no such order'):
fb.success = False
wb.cancel_order('someUUID')
# Note: this can be a bug in exchange.bittrex._validate_response
with pytest.raises(KeyError):
fb.result = {'success': False} # message is missing!
wb.cancel_order('someUUID')
with pytest.raises(btx.OperationalException, match=r'foo'):
fb.result = {'success': False, 'message': 'foo'}
wb.cancel_order('someUUID')
def test_exchange_get_pair_detail_url():
wb = make_wrap_bittrex()
assert wb.get_pair_detail_url('BTC_ETH')
def test_exchange_get_markets():
wb = make_wrap_bittrex()
fb = FakeBittrex()
x = wb.get_markets()
assert x == ['__']
with pytest.raises(btx.OperationalException, match=r'market gone'):
fb.success = False
wb.get_markets()
def test_exchange_get_market_summaries():
wb = make_wrap_bittrex()
fb = FakeBittrex()
assert ['sum'] == wb.get_market_summaries()
with pytest.raises(btx.OperationalException, match=r'no summary'):
fb.success = False
wb.get_market_summaries()
def test_exchange_get_wallet_health():
wb = make_wrap_bittrex()
fb = FakeBittrex()
x = wb.get_wallet_health()
assert x[0]['Currency'] == 'BTC_ETH'
with pytest.raises(btx.OperationalException, match=r'bad health'):
fb.success = False
wb.get_wallet_health()
def test_validate_response_success():
response = {
'message': '',
'result': [],
}
Bittrex._validate_response(response)
def test_validate_response_no_api_response():
response = {
'message': 'NO_API_RESPONSE',
'result': None,
}
with pytest.raises(ContentDecodingError, match=r'.*NO_API_RESPONSE.*'):
Bittrex._validate_response(response)
def test_validate_response_min_trade_requirement_not_met():
response = {
'message': 'MIN_TRADE_REQUIREMENT_NOT_MET',
'result': None,
}
with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'):
Bittrex._validate_response(response)

View File

@@ -3,6 +3,7 @@
import json
import math
import random
import pytest
from copy import deepcopy
from typing import List
from unittest.mock import MagicMock
@@ -11,14 +12,11 @@ import numpy as np
import pandas as pd
from arrow import Arrow
from freqtrade import optimize
from freqtrade import optimize, constants, DependencyException
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments
from freqtrade.arguments import Arguments, TimeRange
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
from freqtrade.tests.conftest import default_conf, log_has
# Avoid to reinit the same object again and again
_BACKTESTING = Backtesting(default_conf())
from freqtrade.tests.conftest import log_has, patch_exchange
def get_args(args) -> List[str]:
@@ -33,50 +31,61 @@ def trim_dictlist(dict_list, num):
def load_data_test(what):
timerange = ((None, 'line'), None, -100)
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange)
pair = data['BTC_UNITEST']
timerange = TimeRange(None, 'line', 0, -101)
data = optimize.load_data(None, ticker_interval='1m',
pairs=['UNITTEST/BTC'], timerange=timerange)
pair = data['UNITTEST/BTC']
datalen = len(pair)
# Depending on the what parameter we now adjust the
# loaded data looks:
# pair :: [{'O': 0.123, 'H': 0.123, 'L': 0.123,
# 'C': 0.123, 'V': 123.123,
# 'T': '2017-11-04T23:02:00', 'BV': 0.123}]
# pair :: [[ 1509836520000, unix timestamp in ms
# 0.00162008, open
# 0.00162008, high
# 0.00162008, low
# 0.00162008, close
# 108.14853839 base volume
# ]]
base = 0.001
if what == 'raise':
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
'O': x * base, # But replace O,H,L,C
'H': x * base + 0.0001,
'L': x * base - 0.0001,
'C': x * base} for x in range(0, datalen)]}
return {'UNITTEST/BTC': [
[
pair[x][0], # Keep old dates
x * base, # But replace O,H,L,C
x * base + 0.0001,
x * base - 0.0001,
x * base,
pair[x][5], # Keep old volume
] for x in range(0, datalen)
]}
if what == 'lower':
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
'O': 1 - x * base, # But replace O,H,L,C
'H': 1 - x * base + 0.0001,
'L': 1 - x * base - 0.0001,
'C': 1 - x * base} for x in range(0, datalen)]}
return {'UNITTEST/BTC': [
[
pair[x][0], # Keep old dates
1 - x * base, # But replace O,H,L,C
1 - x * base + 0.0001,
1 - x * base - 0.0001,
1 - x * base,
pair[x][5] # Keep old volume
] for x in range(0, datalen)
]}
if what == 'sine':
hz = 0.1 # frequency
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
# But replace O,H,L,C
'O': math.sin(x * hz) / 1000 + base,
'H': math.sin(x * hz) / 1000 + base + 0.0001,
'L': math.sin(x * hz) / 1000 + base - 0.0001,
'C': math.sin(x * hz) / 1000 + base} for x in range(0, datalen)]}
return {'UNITTEST/BTC': [
[
pair[x][0], # Keep old dates
math.sin(x * hz) / 1000 + base, # But replace O,H,L,C
math.sin(x * hz) / 1000 + base + 0.0001,
math.sin(x * hz) / 1000 + base - 0.0001,
math.sin(x * hz) / 1000 + base,
pair[x][5] # Keep old volume
] for x in range(0, datalen)
]}
return data
def simple_backtest(config, contour, num_results) -> None:
backtesting = _BACKTESTING
def simple_backtest(config, contour, num_results, mocker) -> None:
patch_exchange(mocker)
backtesting = Backtesting(config)
data = load_data_test(contour)
processed = backtesting.tickerdata_to_dataframe(data)
@@ -93,26 +102,29 @@ def simple_backtest(config, contour, num_results) -> None:
assert len(results) == num_results
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False, timerange=None):
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1, timerange=timerange)
pairdata = {'BTC_UNITEST': tickerdata}
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False,
timerange=None, exchange=None):
tickerdata = optimize.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
pairdata = {'UNITTEST/BTC': tickerdata}
return pairdata
# use for mock freqtrade.exchange.get_ticker_history'
def _load_pair_as_ticks(pair, tickfreq):
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
ticks = trim_dictlist(ticks, -200)
ticks = trim_dictlist(ticks, -201)
return ticks[pair]
# FIX: fixturize this?
def _make_backtest_conf(conf=None, pair='BTC_UNITEST', record=None):
data = optimize.load_data(None, ticker_interval=8, pairs=[pair])
data = trim_dictlist(data, -200)
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
data = trim_dictlist(data, -201)
patch_exchange(mocker)
backtesting = Backtesting(conf)
return {
'stake_amount': conf['stake_amount'],
'processed': _BACKTESTING.tickerdata_to_dataframe(data),
'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 10,
'realistic': True,
'record': record
@@ -148,21 +160,6 @@ def _trend_alternate(dataframe=None):
return dataframe
def _run_backtest_1(fun, backtest_conf):
# strategy is a global (hidden as a singleton), so we
# emulate strategy being pure, by override/restore here
# if we dont do this, the override in strategy will carry over
# to other tests
old_buy = _BACKTESTING.populate_buy_trend
old_sell = _BACKTESTING.populate_sell_trend
_BACKTESTING.populate_buy_trend = fun # Override
_BACKTESTING.populate_sell_trend = fun # Override
results = _BACKTESTING.backtest(backtest_conf)
_BACKTESTING.populate_buy_trend = old_buy # restore override
_BACKTESTING.populate_sell_trend = old_sell # restore override
return results
# Unit tests
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
"""
@@ -186,7 +183,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
@@ -218,12 +215,13 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1',
'--ticker-interval', '1m',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
'--timerange', ':100',
'--export', '/bar/foo'
'--export', '/bar/foo',
'--export-filename', 'foo_bar.json'
]
config = setup_configuration(get_args(args))
@@ -234,24 +232,24 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
'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: 1 ...',
'Using ticker_interval: 1m ...',
caplog.record_tuples
)
assert 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation'in config
assert 'realistic_simulation' in config
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert log_has('Using max_open_trades: 1 ...', 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 'timerange' in config
assert log_has(
@@ -264,13 +262,42 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
assert 'exportfilename' in config
assert log_has(
'Storing backtest results to {} ...'.format(config['exportfilename']),
caplog.record_tuples
)
def test_start(mocker, default_conf, caplog) -> None:
def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
"""
Test setup_configuration() function
"""
conf = deepcopy(default_conf)
conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'backtesting'
]
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
setup_configuration(get_args(args))
def test_start(mocker, fee, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
@@ -289,114 +316,104 @@ def test_start(mocker, default_conf, caplog) -> None:
assert start_mock.call_count == 1
def test_backtesting__init__(mocker, default_conf) -> None:
"""
Test Backtesting.__init__() method
"""
init_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._init', init_mock)
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert backtesting.analyze is None
assert backtesting.ticker_interval is None
assert backtesting.tickerdata_to_dataframe is None
assert backtesting.populate_buy_trend is None
assert backtesting.populate_sell_trend is None
assert init_mock.call_count == 1
def test_backtesting_init(default_conf) -> None:
def test_backtesting_init(mocker, default_conf) -> None:
"""
Test Backtesting._init() method
"""
patch_exchange(mocker)
get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5))
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert isinstance(backtesting.analyze, Analyze)
assert backtesting.ticker_interval == 5
assert backtesting.ticker_interval == '5m'
assert callable(backtesting.tickerdata_to_dataframe)
assert callable(backtesting.populate_buy_trend)
assert callable(backtesting.populate_sell_trend)
get_fee.assert_called()
assert backtesting.fee == 0.5
def test_tickerdata_to_dataframe(default_conf) -> None:
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
"""
Test Backtesting.tickerdata_to_dataframe() method
"""
patch_exchange(mocker)
timerange = TimeRange(None, 'line', 0, -100)
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': tick}
timerange = ((None, 'line'), None, -100)
tick = optimize.load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
backtesting = _BACKTESTING
backtesting = Backtesting(default_conf)
data = backtesting.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100
assert len(data['UNITTEST/BTC']) == 99
# Load Analyze to compare the result between Backtesting function and Analyze are the same
analyze = Analyze(default_conf)
data2 = analyze.tickerdata_to_dataframe(tickerlist)
assert data['BTC_UNITEST'].equals(data2['BTC_UNITEST'])
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
def test_get_timeframe() -> None:
def test_get_timeframe(default_conf, mocker) -> None:
"""
Test Backtesting.get_timeframe() method
"""
backtesting = _BACKTESTING
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
data = backtesting.tickerdata_to_dataframe(
optimize.load_data(
None,
ticker_interval=1,
pairs=['BTC_UNITEST']
ticker_interval='1m',
pairs=['UNITTEST/BTC']
)
)
min_date, max_date = backtesting.get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
def test_generate_text_table():
def test_generate_text_table(default_conf, mocker):
"""
Test Backtesting.generate_text_table() method
"""
backtesting = _BACKTESTING
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
results = pd.DataFrame(
{
'currency': ['BTC_ETH', 'BTC_ETH'],
'pair': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4],
'duration': [10, 30],
'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30],
'profit': [2, 0],
'loss': [0, 0]
}
)
result_str = (
'pair buy count avg profit % '
'total profit BTC avg duration profit loss\n'
'------- ----------- -------------- '
'------------------ -------------- -------- ------\n'
'BTC_ETH 2 15.00 '
'0.60000000 20.0 2 0\n'
'TOTAL 2 15.00 '
'0.60000000 20.0 2 0'
'| pair | buy count | avg profit % | '
'total profit BTC | avg duration | profit | loss |\n'
'|:--------|------------:|---------------:|'
'-------------------:|---------------:|---------:|-------:|\n'
'| ETH/BTC | 2 | 15.00 | '
'0.60000000 | 20.0 | 2 | 0 |\n'
'| TOTAL | 2 | 15.00 | '
'0.60000000 | 20.0 | 2 | 0 |'
)
assert backtesting._generate_text_table(data={'BTC_ETH': {}}, 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:
"""
Test Backtesting.start() method
"""
def get_timeframe(input1, input2):
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.exchange.get_ticker_history')
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history')
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
@@ -405,7 +422,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['ticker_interval'] = 1
conf['live'] = False
conf['datadir'] = None
@@ -426,13 +443,49 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
assert log_has(line, caplog.record_tuples)
def test_backtest(default_conf) -> 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):
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.exchange.Exchange.get_ticker_history')
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
_generate_text_table=MagicMock(return_value='1'),
get_timeframe=get_timeframe,
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['ticker_interval'] = "1m"
conf['live'] = False
conf['datadir'] = None
conf['export'] = None
conf['timerange'] = '20180101-20180102'
backtesting = Backtesting(conf)
backtesting.start()
# check the logs, that will contain the backtest result
assert log_has('No data found. Terminating.', caplog.record_tuples)
def test_backtest(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method
"""
backtesting = _BACKTESTING
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200)
results = backtesting.backtest(
{
@@ -443,16 +496,19 @@ def test_backtest(default_conf) -> None:
}
)
assert not results.empty
assert len(results) == 2
def test_backtest_1min_ticker_interval(default_conf) -> None:
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method with 1 min ticker
"""
backtesting = _BACKTESTING
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
# Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200)
results = backtesting.backtest(
{
@@ -463,17 +519,19 @@ def test_backtest_1min_ticker_interval(default_conf) -> None:
}
)
assert not results.empty
assert len(results) == 1
def test_processed() -> None:
def test_processed(default_conf, mocker) -> None:
"""
Test Backtesting.backtest() method with offline data
"""
backtesting = _BACKTESTING
patch_exchange(mocker)
backtesting = Backtesting(default_conf)
dict_of_tickerrows = load_data_test('raise')
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
dataframe = dataframes['BTC_UNITEST']
dataframe = dataframes['UNITTEST/BTC']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
for col in ['close', 'high', 'low', 'open', 'date',
@@ -481,76 +539,109 @@ def test_processed() -> None:
assert col in cols
def test_backtest_pricecontours(default_conf) -> None:
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
tests = [['raise', 18], ['lower', 0], ['sine', 16]]
for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres)
simple_backtest(default_conf, contour, numres, mocker)
# Test backtest using offline data (testdata directory)
def test_backtest_ticks(default_conf):
def test_backtest_ticks(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
patch_exchange(mocker)
ticks = [1, 5]
fun = _BACKTESTING.populate_buy_trend
for tick in ticks:
backtest_conf = _make_backtest_conf(conf=default_conf)
results = _run_backtest_1(fun, backtest_conf)
fun = Backtesting(default_conf).populate_buy_trend
for _ in ticks:
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert not results.empty
def test_backtest_clash_buy_sell(default_conf):
# Override the default buy trend function in our DefaultStrategy
def test_backtest_clash_buy_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy
def fun(dataframe=None):
buy_value = 1
sell_value = 1
return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(conf=default_conf)
results = _run_backtest_1(fun, backtest_conf)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert results.empty
def test_backtest_only_sell(default_conf):
# Override the default buy trend function in our DefaultStrategy
def test_backtest_only_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy
def fun(dataframe=None):
buy_value = 0
sell_value = 1
return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(conf=default_conf)
results = _run_backtest_1(fun, backtest_conf)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert results.empty
def test_backtest_alternate_buy_sell(default_conf):
backtest_conf = _make_backtest_conf(conf=default_conf, pair='BTC_UNITEST')
results = _run_backtest_1(_trend_alternate, backtest_conf)
assert len(results) == 3
def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override
results = backtesting.backtest(backtest_conf)
backtesting._store_backtest_result("test_.json", results)
assert len(results) == 4
# One trade was force-closed at the end
assert len(results.loc[results.open_at_end]) == 1
def test_backtest_record(default_conf, mocker):
def test_backtest_record(default_conf, fee, mocker):
names = []
records = []
patch_exchange(mocker)
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
mocker.patch(
'freqtrade.optimize.backtesting.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r))
)
backtest_conf = _make_backtest_conf(
conf=default_conf,
pair='BTC_UNITEST',
record="trades"
)
results = _run_backtest_1(_trend_alternate, backtest_conf)
assert len(results) == 3
backtesting = Backtesting(default_conf)
results = pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC",
"UNITTEST/BTC", "UNITTEST/BTC"],
"profit_percent": [0.003312, 0.010801, 0.013803, 0.002780],
"profit_abs": [0.000003, 0.000011, 0.000014, 0.000003],
"open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime,
Arrow(2017, 11, 14, 21, 36, 00).datetime,
Arrow(2017, 11, 14, 22, 12, 00).datetime,
Arrow(2017, 11, 14, 22, 44, 00).datetime],
"close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime,
Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_index": [1, 119, 153, 185],
"close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14]})
backtesting._store_backtest_result("backtest-result.json", results)
assert len(results) == 4
# Assert file_dump_json was only called once
assert names == ['backtest-result.json']
records = records[0]
# Ensure records are of correct type
assert len(records) == 3
# ('BTC_UNITEST', 0.00331158, '1510684320', '1510691700', 0, 117)
assert len(records) == 4
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records
oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur) in records:
assert pair == 'BTC_UNITEST'
assert pair == 'UNITTEST/BTC'
isinstance(profit, float)
# FIX: buy/sell should be converted to ints
isinstance(date_buy, str)
@@ -563,13 +654,15 @@ def test_backtest_record(default_conf, mocker):
def test_backtest_start_live(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
mocker.patch('freqtrade.exchange.get_ticker_history',
new=lambda n, i: _load_pair_as_ticks(n, i))
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history',
new=lambda s, n, i: _load_pair_as_ticks(n, i))
patch_exchange(mocker)
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
read_data=json.dumps(conf)
))
args = MagicMock()
@@ -584,26 +677,29 @@ def test_backtest_start_live(default_conf, mocker, caplog):
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', 'freqtrade/tests/testdata',
'backtesting',
'--ticker-interval', '1',
'--ticker-interval', '1m',
'--live',
'--timerange', '-100'
'--timerange', '-100',
'--realistic-simulation'
]
args = get_args(args)
start(args)
# check the logs, that will contain the backtest result
exists = [
'Parameter -i/--ticker-interval detected ...',
'Using ticker_interval: 1 ...',
'Using ticker_interval: 1m ...',
'Parameter -l/--live detected ...',
'Using max_open_trades: 1 ...',
'Parameter --timerange detected: -100 ..',
'Parameter --datadir detected: freqtrade/tests/testdata ...',
'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:32:00+00:00 up to 2017-11-14T22:59: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 ...'
]
for line in exists:
log_has(line, caplog.record_tuples)
assert log_has(line, caplog.record_tuples)

View File

@@ -1,20 +1,30 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
import json
import os
import signal
from copy import deepcopy
from unittest.mock import MagicMock
import pandas as pd
import pytest
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.optimize.hyperopt import Hyperopt, start
from freqtrade.strategy.resolver import StrategyResolver
from freqtrade.tests.conftest import default_conf, log_has
from freqtrade.tests.conftest import log_has, patch_exchange
from freqtrade.tests.optimize.test_backtesting import get_args
# Avoid to reinit the same object again and again
_HYPEROPT = Hyperopt(default_conf())
_HYPEROPT_INITIALIZED = False
_HYPEROPT = None
@pytest.fixture(scope='function')
def init_hyperopt(default_conf, mocker):
global _HYPEROPT_INITIALIZED, _HYPEROPT
if not _HYPEROPT_INITIALIZED:
patch_exchange(mocker)
_HYPEROPT = Hyperopt(default_conf)
_HYPEROPT_INITIALIZED = True
# Functions for recurrent object patching
@@ -50,10 +60,13 @@ def test_start(mocker, default_conf, caplog) -> None:
Test start() function
"""
start_mock = MagicMock()
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
patch_exchange(mocker)
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
@@ -74,7 +87,7 @@ def test_start(mocker, default_conf, caplog) -> None:
assert start_mock.call_count == 1
def test_loss_calculation_prefer_correct_trade_count() -> None:
def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
"""
Test Hyperopt.calculate_loss()
"""
@@ -88,7 +101,7 @@ def test_loss_calculation_prefer_correct_trade_count() -> None:
assert under > correct
def test_loss_calculation_prefer_shorter_trades() -> None:
def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None:
"""
Test Hyperopt.calculate_loss()
"""
@@ -99,7 +112,7 @@ def test_loss_calculation_prefer_shorter_trades() -> None:
assert shorter < longer
def test_loss_calculation_has_limited_profit() -> None:
def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
hyperopt = _HYPEROPT
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
@@ -109,7 +122,7 @@ def test_loss_calculation_has_limited_profit() -> None:
assert under > correct
def test_log_results_if_loss_improves(capsys) -> None:
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2
hyperopt.log_results(
@@ -124,7 +137,7 @@ def test_log_results_if_loss_improves(capsys) -> None:
assert ' 1/2: foo. Loss 1.00000'in out
def test_no_log_if_loss_does_not_improve(caplog) -> None:
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2
hyperopt.log_results(
@@ -135,7 +148,7 @@ def test_no_log_if_loss_does_not_improve(caplog) -> None:
assert caplog.record_tuples == []
def test_fmin_best_results(mocker, default_conf, caplog) -> None:
def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None:
fmin_result = {
"macd_below_zero": 0,
"adx": 1,
@@ -168,7 +181,7 @@ def test_fmin_best_results(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
patch_exchange(mocker)
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
@@ -203,7 +216,7 @@ def test_fmin_best_results(mocker, default_conf, caplog) -> None:
assert line in caplog.text
def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
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())
@@ -212,7 +225,8 @@ def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
conf.update({'epochs': 1})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
patch_exchange(mocker)
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
@@ -230,13 +244,12 @@ def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
assert line in caplog.text
def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> None:
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({'mongodb': False})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
@@ -253,7 +266,7 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> No
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={})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
patch_exchange(mocker)
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
@@ -272,7 +285,7 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> No
assert total_tries == (current_tries + len(trials.results))
def test_save_trials_saves_trials(mocker, caplog) -> None:
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)
@@ -281,22 +294,24 @@ def test_save_trials_saves_trials(mocker, caplog) -> None:
hyperopt.save_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has(
'Saving Trials to \'freqtrade/tests/optimize/ut_trials.pickle\'',
'Saving Trials to \'{}\''.format(trials_file),
caplog.record_tuples
)
mock_dump.assert_called_once()
def test_read_trials_returns_trials_file(mocker, caplog) -> None:
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
trials = create_trials(mocker)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
hyperopt = _HYPEROPT
hyperopt_trial = hyperopt.read_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has(
'Reading Trials from \'freqtrade/tests/optimize/ut_trials.pickle\'',
'Reading Trials from \'{}\''.format(trials_file),
caplog.record_tuples
)
assert hyperopt_trial == trials
@@ -304,7 +319,7 @@ def test_read_trials_returns_trials_file(mocker, caplog) -> None:
mock_load.assert_called_once()
def test_roi_table_generation() -> None:
def test_roi_table_generation(init_hyperopt) -> None:
params = {
'roi_t1': 5,
'roi_t2': 10,
@@ -318,16 +333,16 @@ def test_roi_table_generation() -> None:
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
def test_start_calls_fmin(mocker, default_conf) -> None:
def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
patch_exchange(mocker)
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'mongodb': False})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
@@ -339,50 +354,37 @@ def test_start_calls_fmin(mocker, default_conf) -> None:
mock_fmin.assert_called_once()
def test_start_uses_mongotrials(mocker, default_conf) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
mock_mongotrials = mocker.patch(
'freqtrade.optimize.hyperopt.MongoTrials',
return_value=create_trials(mocker)
)
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'mongodb': True})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
hyperopt = Hyperopt(conf)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_mongotrials.assert_called_once()
mock_fmin.assert_called_once()
# test log_trials_result
# test buy_strategy_generator def populate_buy_trend
# test optimizer if 'ro_t1' in params
def test_format_results():
def test_format_results(init_hyperopt):
"""
Test Hyperopt.format_results()
"""
# Test with BTC as stake_currency
trades = [
('BTC_ETH', 2, 2, 123),
('BTC_LTC', 1, 1, 123),
('BTC_XRP', -1, -2, -246)
('ETH/BTC', 2, 2, 123),
('LTC/BTC', 1, 1, 123),
('XPR/BTC', -1, -2, -246)
]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
df = pd.DataFrame.from_records(trades, columns=labels)
x = Hyperopt.format_results(df)
assert x.find(' 66.67%')
result = _HYPEROPT.format_results(df)
assert result.find(' 66.67%')
assert result.find('Total profit 1.00000000 BTC')
assert result.find('2.0000Σ %')
# Test with EUR as stake_currency
trades = [
('ETH/EUR', 2, 2, 123),
('LTC/EUR', 1, 1, 123),
('XPR/EUR', -1, -2, -246)
]
df = pd.DataFrame.from_records(trades, columns=labels)
result = _HYPEROPT.format_results(df)
assert result.find('Total profit 1.00000000 EUR')
def test_signal_handler(mocker):
def test_signal_handler(mocker, init_hyperopt):
"""
Test Hyperopt.signal_handler()
"""
@@ -392,11 +394,11 @@ def test_signal_handler(mocker):
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
hyperopt = _HYPEROPT
hyperopt.signal_handler(9, None)
hyperopt.signal_handler(signal.SIGTERM, None)
assert m.call_count == 3
def test_has_space():
def test_has_space(init_hyperopt):
"""
Test Hyperopt.has_space() method
"""
@@ -409,14 +411,14 @@ def test_has_space():
assert _HYPEROPT.has_space('buy')
def test_populate_indicators() -> None:
def test_populate_indicators(init_hyperopt) -> None:
"""
Test Hyperopt.populate_indicators()
"""
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerlist = {'BTC_UNITEST': tick}
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
# Check if some indicators are generated. We will not test all of them
assert 'adx' in dataframe
@@ -424,14 +426,14 @@ def test_populate_indicators() -> None:
assert 'cci' in dataframe
def test_buy_strategy_generator() -> None:
def test_buy_strategy_generator(init_hyperopt) -> None:
"""
Test Hyperopt.buy_strategy_generator()
"""
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerlist = {'BTC_UNITEST': tick}
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
{
@@ -481,7 +483,7 @@ def test_buy_strategy_generator() -> None:
assert 1 in result['buy']
def test_generate_optimizer(mocker, default_conf) -> None:
def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
"""
Test Hyperopt.generate_optimizer() function
"""
@@ -491,15 +493,16 @@ def test_generate_optimizer(mocker, default_conf) -> None:
conf.update({'spaces': 'all'})
trades = [
('BTC_POWR', 0.023117, 0.000233, 100)
('POWR/BTC', 0.023117, 0.000233, 100)
]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration']
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
MagicMock(return_value=backtest_result)
)
patch_exchange(mocker)
optimizer_param = {
'adx': {'enabled': False},

View File

@@ -1,16 +0,0 @@
# pragma pylint: disable=missing-docstring,W0212
from user_data.hyperopt_conf import hyperopt_optimize_conf
def test_hyperopt_optimize_conf():
hyperopt_conf = hyperopt_optimize_conf()
assert "max_open_trades" in hyperopt_conf
assert "stake_currency" in hyperopt_conf
assert "stake_amount" in hyperopt_conf
assert "minimal_roi" in hyperopt_conf
assert "stoploss" in hyperopt_conf
assert "bid_strategy" in hyperopt_conf
assert "exchange" in hyperopt_conf
assert "pair_whitelist" in hyperopt_conf['exchange']

View File

@@ -3,15 +3,18 @@
import json
import os
import uuid
import arrow
from shutil import copyfile
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
from freqtrade.tests.conftest import log_has
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
load_cached_data_for_updating
from freqtrade.arguments import TimeRange
from freqtrade.tests.conftest import log_has, get_patched_exchange
# Change this if modifying BTC_UNITEST testdatafile
# Change this if modifying UNITTEST/BTC testdatafile
_BTC_UNITTEST_LENGTH = 13681
@@ -46,31 +49,30 @@ def _clean_test_file(file: str) -> None:
os.rename(file_swp, file)
def test_load_data_30min_ticker(ticker_history, mocker, caplog) -> None:
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
"""
Test load_data() with 30 min ticker
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
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')
_backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['BTC_UNITTEST'], ticker_interval=30)
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
assert os.path.isfile(file) is True
assert not log_has('Download the pair: "BTC_ETH", Interval: 30 min', caplog.record_tuples)
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 30m', caplog.record_tuples)
_clean_test_file(file)
def test_load_data_5min_ticker(ticker_history, mocker, caplog) -> None:
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
"""
Test load_data() with 5 min ticker
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
_backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['BTC_ETH'], ticker_interval=5)
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='5m')
assert os.path.isfile(file) is True
assert not log_has('Download the pair: "BTC_ETH", Interval: 5 min', caplog.record_tuples)
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 5m', caplog.record_tuples)
_clean_test_file(file)
@@ -78,27 +80,43 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
"""
Test load_data() with 1 min ticker
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
_backup_file(file, copy_file=True)
optimize.load_data(None, ticker_interval=1, pairs=['BTC_ETH'])
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
assert os.path.isfile(file) is True
assert not log_has('Download the pair: "BTC_ETH", Interval: 1 min', caplog.record_tuples)
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 1m', caplog.record_tuples)
_clean_test_file(file)
def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_conf) -> None:
"""
Test load_data() with 1 min ticker
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf)
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
_backup_file(file)
optimize.load_data(None, ticker_interval=1, pairs=['BTC_MEME'])
# do not download a new pair if refresh_pairs isn't set
optimize.load_data(None,
ticker_interval='1m',
refresh_pairs=False,
pairs=['MEME/BTC'])
assert os.path.isfile(file) is False
assert log_has('No data for pair: "MEME/BTC", Interval: 1m. '
'Use --refresh-pairs-cached to download the data',
caplog.record_tuples)
# download a new pair if refresh_pairs is set
optimize.load_data(None,
ticker_interval='1m',
refresh_pairs=True,
exchange=exchange,
pairs=['MEME/BTC'])
assert os.path.isfile(file) is True
assert log_has('Download the pair: "BTC_MEME", Interval: 1 min', caplog.record_tuples)
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
_clean_test_file(file)
@@ -106,13 +124,13 @@ def test_testdata_path() -> None:
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
def test_download_pairs(ticker_history, mocker) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
file2_1 = 'freqtrade/tests/testdata/BTC_CFI-1.json'
file2_5 = 'freqtrade/tests/testdata/BTC_CFI-5.json'
def test_download_pairs(ticker_history, mocker, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf)
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')
file2_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-1m.json')
file2_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-5m.json')
_backup_file(file1_1)
_backup_file(file1_5)
@@ -122,7 +140,8 @@ def test_download_pairs(ticker_history, mocker) -> None:
assert os.path.isfile(file1_1) is False
assert os.path.isfile(file2_1) is False
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=1) is True
assert download_pairs(None, exchange,
pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='1m') is True
assert os.path.isfile(file1_1) is True
assert os.path.isfile(file2_1) is True
@@ -134,7 +153,8 @@ def test_download_pairs(ticker_history, mocker) -> None:
assert os.path.isfile(file1_5) is False
assert os.path.isfile(file2_5) is False
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI'], ticker_interval=5) is True
assert download_pairs(None, exchange,
pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='5m') is True
assert os.path.isfile(file1_5) is True
assert os.path.isfile(file2_5) is True
@@ -144,91 +164,201 @@ def test_download_pairs(ticker_history, mocker) -> None:
_clean_test_file(file2_5)
def test_download_pairs_exception(ticker_history, mocker, caplog) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
def test_load_cached_data_for_updating(mocker) -> None:
datadir = os.path.join(os.path.dirname(__file__), '..', 'testdata')
test_data = None
test_filename = os.path.join(datadir, 'UNITTEST_BTC-1m.json')
with open(test_filename, "rt") as file:
test_data = json.load(file)
# change now time to test 'line' cases
# now = last cached item + 1 hour
now_ts = test_data[-1][0] / 1000 + 60 * 60
mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts))
# timeframe starts earlier than the cached data
# should fully update data
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == []
assert start_ts == test_data[0][0] - 1000
# same with 'line' timeframe
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
TimeRange(None, 'line', 0, -num_lines))
assert data == []
assert start_ts < test_data[0][0] - 1
# timeframe starts in the center of the cached data
# should return the chached data w/o the last item
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# same with 'line' timeframe
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# timeframe starts after the chached data
# should return the chached data w/o the last item
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# same with 'line' timeframe
num_lines = 30
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# no timeframe is set
# should return the chached data w/o the last item
num_lines = 30
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# no datafile exist
# should return timestamp start time
timerange = TimeRange('date', None, now_ts - 10000, 0)
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
timerange)
assert data == []
assert start_ts == (now_ts - 10000) * 1000
# same with 'line' timeframe
num_lines = 30
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
timerange)
assert data == []
assert start_ts == (now_ts - num_lines * 60) * 1000
# no datafile exist, no timeframe is set
# should return an empty array and None
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
None)
assert data == []
assert start_ts is 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.optimize.__init__.download_backtesting_testdata',
side_effect=BaseException('File Error'))
exchange = get_patched_exchange(mocker, default_conf)
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.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')
_backup_file(file1_1)
_backup_file(file1_5)
download_pairs(None, pairs=['BTC-MEME'], ticker_interval=1)
download_pairs(None, exchange, pairs=['MEME/BTC'], ticker_interval='1m')
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file1_5)
assert log_has('Failed to download the pair: "BTC-MEME", Interval: 1 min', caplog.record_tuples)
assert log_has('Failed to download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
def test_download_backtesting_testdata(ticker_history, mocker) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history)
exchange = get_patched_exchange(mocker, default_conf)
# Download a 1 min ticker file
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
file1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'XEL_BTC-1m.json')
_backup_file(file1)
download_backtesting_testdata(None, pair="BTC-XEL", interval=1)
download_backtesting_testdata(None, exchange, pair="XEL/BTC", tick_interval='1m')
assert os.path.isfile(file1) is True
_clean_test_file(file1)
# Download a 5 min ticker file
file2 = 'freqtrade/tests/testdata/BTC_STORJ-5.json'
file2 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'STORJ_BTC-5m.json')
_backup_file(file2)
download_backtesting_testdata(None, pair="BTC-STORJ", interval=5)
download_backtesting_testdata(None, exchange, pair="STORJ/BTC", tick_interval='5m')
assert os.path.isfile(file2) is True
_clean_test_file(file2)
def test_download_backtesting_testdata2(mocker) -> None:
tick = [{'T': 'bar'}, {'T': 'foo'}]
def test_download_backtesting_testdata2(mocker, default_conf) -> None:
tick = [
[1509836520000, 0.00162008, 0.00162008, 0.00162008, 0.00162008, 108.14853839],
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
]
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
download_backtesting_testdata(None, pair="BTC-UNITEST", interval=1)
download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=tick)
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='3m')
assert json_dump_mock.call_count == 2
def test_load_tickerdata_file() -> None:
# 7 does not exist in either format.
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
assert not load_tickerdata_file(None, 'UNITTEST/BTC', '7m')
# 1 exists only as a .json
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 8)
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '8m')
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
def test_init(default_conf, mocker) -> None:
conf = {'exchange': {'pair_whitelist': []}}
mocker.patch('freqtrade.optimize.hyperopt_optimize_conf', return_value=conf)
exchange = get_patched_exchange(mocker, default_conf)
assert {} == optimize.load_data(
'',
exchange=exchange,
pairs=[],
refresh_pairs=True,
ticker_interval=int(default_conf['ticker_interval'])
ticker_interval=default_conf['ticker_interval']
)
def test_trim_tickerlist() -> None:
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
with open(file) as data_file:
ticker_list = json.load(data_file)
ticker_list_len = len(ticker_list)
# Test the pattern ^(-\d+)$
# This pattern remove X element from the beginning
timerange = ((None, 'line'), None, 5)
# This pattern uses the latest N elements
timerange = TimeRange(None, 'line', 0, -5)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_list_len == ticker_len + 5
assert ticker_len == 5
assert ticker_list[0] is not ticker[0] # The first element should be different
assert ticker_list[-1] is ticker[-1] # The last element must be the same
# Test the pattern ^(\d+)-$
# This pattern keep X element from the end
timerange = (('line', None), 5, None)
timerange = TimeRange('line', None, 5, 0)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
@@ -238,7 +368,7 @@ def test_trim_tickerlist() -> None:
# Test the pattern ^(\d+)-(\d+)$
# This pattern extract a window
timerange = (('index', 'index'), 5, 10)
timerange = TimeRange('index', 'index', 5, 10)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
@@ -247,9 +377,40 @@ def test_trim_tickerlist() -> None:
assert ticker_list[5] is ticker[0] # The list starts at the index 5
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
# Test the pattern ^(\d{8})-(\d{8})$
# This pattern extract a window between the dates
timerange = TimeRange('date', 'date', ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == 5
assert ticker_list[0] is not ticker[0] # The first element should be different
assert ticker_list[5] is ticker[0] # The list starts at the index 5
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
# Test the pattern ^-(\d{8})$
# This pattern extracts elements from the start to the date
timerange = TimeRange(None, 'date', 0, ticker_list[10][0] / 1000 - 1)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == 10
assert ticker_list[0] is ticker[0] # The start of the list is included
assert ticker_list[9] is ticker[-1] # The element 10 is not included
# Test the pattern ^(\d{8})-$
# This pattern extracts elements from the date to now
timerange = TimeRange('date', None, ticker_list[10][0] / 1000 - 1, None)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == ticker_list_len - 10
assert ticker_list[10] is ticker[0] # The first element is element #10
assert ticker_list[-1] is ticker[-1] # The last element is the same
# Test a wrong pattern
# This pattern must return the list unchanged
timerange = ((None, None), None, 5)
timerange = TimeRange(None, None, None, 5)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
@@ -261,7 +422,8 @@ def test_file_dump_json() -> None:
Test file_dump_json()
:return: None
"""
file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4()))
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
'test_{id}.json'.format(id=str(uuid.uuid4())))
data = {'bar': 'foo'}
# check the file we will create does not exist

View File

@@ -7,11 +7,11 @@ Unit test file for rpc/rpc.py
from datetime import datetime
from unittest.mock import MagicMock
from sqlalchemy import create_engine
import pytest
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC
from freqtrade.rpc.rpc import RPC, RPCException
from freqtrade.state import State
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
@@ -25,41 +25,40 @@ def prec_satoshi(a, b) -> float:
# Unit tests
def test_rpc_trade_status(default_conf, ticker, 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)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_trade_status()
assert error
assert 'trader is not running' in result
with pytest.raises(RPCException, match=r'.*trader is not running*'):
rpc._rpc_trade_status()
freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_trade_status()
assert error
assert 'no active trade' in result
with pytest.raises(RPCException, match=r'.*no active trade*'):
rpc._rpc_trade_status()
freqtradebot.create_trade()
(error, result) = rpc.rpc_trade_status()
assert not error
trade = result[0]
trades = rpc._rpc_trade_status()
trade = trades[0]
result_message = [
'*Trade ID:* `1`\n'
'*Current Pair:* '
'[BTC_ETH](https://www.bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
'[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
'*Open Since:* `just now`\n'
'*Amount:* `90.99181074`\n'
'*Open Rate:* `0.00001099`\n'
@@ -67,60 +66,62 @@ def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
'*Current Rate:* `0.00001098`\n'
'*Close Profit:* `None`\n'
'*Current Profit:* `-0.59%`\n'
'*Open Order:* `(LIMIT_BUY rem=0.00000000)`'
'*Open Order:* `(limit buy rem=0.00000000)`'
]
assert result == result_message
assert trade.find('[BTC_ETH]') >= 0
assert trades == result_message
assert trade.find('[ETH/BTC]') >= 0
def test_rpc_status_table(default_conf, ticker, 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)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_status_table()
assert error
assert '*Status:* `trader is not running`' in result
with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'):
rpc._rpc_status_table()
freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_status_table()
assert error
assert '*Status:* `no active order`' in result
with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'):
rpc._rpc_status_table()
freqtradebot.create_trade()
(error, result) = rpc.rpc_status_table()
result = rpc._rpc_status_table()
assert 'just now' in result['Since'].all()
assert 'BTC_ETH' in result['Pair'].all()
assert 'ETH/BTC' in result['Pair'].all()
assert '-0.59%' in result['Profit'].all()
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker)\
-> None:
def test_rpc_daily_profit(default_conf, update, ticker, fee,
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})
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
@@ -139,8 +140,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
# Try valid data
update.message.text = '/daily 2'
(error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert not error
days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert len(days) == 7
for day in days:
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
@@ -153,13 +153,12 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
assert str(days[0][0]) == str(datetime.utcnow().date())
# Try invalid data
(error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency)
assert error
assert days.find('must be an integer greater than 0') >= 0
with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
def test_rpc_trade_statistics(
default_conf, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker) -> None:
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test rpc_trade_statistics() method
"""
@@ -169,22 +168,23 @@ def test_rpc_trade_statistics(
ticker=MagicMock(return_value={'price_usd': 15000.0}),
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
rpc = RPC(freqtradebot)
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
assert error
assert stats.find('no closed trade') >= 0
with pytest.raises(RPCException, match=r'.*no closed trade*'):
rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
# Create some test data
freqtradebot.create_trade()
@@ -194,7 +194,7 @@ def test_rpc_trade_statistics(
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up
)
@@ -202,26 +202,40 @@ def test_rpc_trade_statistics(
trade.close_date = datetime.utcnow()
trade.is_open = False
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
assert not error
freqtradebot.create_trade()
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up
)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
assert prec_satoshi(stats['profit_all_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_all_percent'], 6.2)
assert prec_satoshi(stats['profit_all_fiat'], 0.93255)
assert stats['trade_count'] == 1
assert prec_satoshi(stats['profit_all_coin'], 5.632e-05)
assert prec_satoshi(stats['profit_all_percent'], 2.81)
assert prec_satoshi(stats['profit_all_fiat'], 0.8448)
assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
assert stats['best_pair'] == 'BTC_ETH'
assert stats['best_pair'] == 'ETH/BTC'
assert prec_satoshi(stats['best_rate'], 6.2)
# Test that rpc_trade_statistics can handle trades that lacks
# trade.open_rate (it is set to None)
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_up, limit_buy_order,
limit_sell_order):
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
ticker_sell_up, limit_buy_order, limit_sell_order):
"""
Test rpc_trade_statistics() method
"""
@@ -231,14 +245,16 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
ticker=MagicMock(return_value={'price_usd': 15000.0}),
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
@@ -251,9 +267,10 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
trade.update(limit_buy_order)
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up
get_ticker=ticker_sell_up,
get_fee=fee
)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
@@ -262,8 +279,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
for trade in Trade.query.order_by(Trade.id).all():
trade.open_rate = None
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
assert not error
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent'], 0)
assert prec_satoshi(stats['profit_closed_fiat'], 0)
@@ -274,7 +290,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
assert stats['best_pair'] == 'BTC_ETH'
assert stats['best_pair'] == 'ETH/BTC'
assert prec_satoshi(stats['best_rate'], 6.2)
@@ -282,22 +298,18 @@ def test_rpc_balance_handle(default_conf, mocker):
"""
Test rpc_balance() method
"""
mock_balance = [
{
'Currency': 'BTC',
'Balance': 10.0,
'Available': 12.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
mock_balance = {
'BTC': {
'free': 10.0,
'total': 12.0,
'used': 2.0,
},
{
'Currency': 'ETH',
'Balance': 0.0,
'Available': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
'ETH': {
'free': 0.0,
'total': 0.0,
'used': 0.0,
}
]
}
patch_get_signal(mocker, (True, False))
mocker.patch.multiple(
@@ -305,28 +317,26 @@ def test_rpc_balance_handle(default_conf, mocker):
ticker=MagicMock(return_value={'price_usd': 15000.0}),
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=mock_balance)
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
assert not error
(trade, x, y, z) = res
assert prec_satoshi(x, 10)
assert prec_satoshi(z, 150000)
assert 'USD' in y
assert len(trade) == 1
assert 'BTC' in trade[0]['currency']
assert prec_satoshi(trade[0]['available'], 12)
assert prec_satoshi(trade[0]['balance'], 10)
assert prec_satoshi(trade[0]['pending'], 0)
assert prec_satoshi(trade[0]['est_btc'], 10)
output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency'])
assert prec_satoshi(total, 12)
assert prec_satoshi(value, 180000)
assert 'USD' in symbol
assert len(output) == 1
assert 'BTC' in output[0]['currency']
assert prec_satoshi(output[0]['available'], 10)
assert prec_satoshi(output[0]['balance'], 12)
assert prec_satoshi(output[0]['pending'], 2)
assert prec_satoshi(output[0]['est_btc'], 12)
def test_rpc_start(mocker, default_conf) -> None:
@@ -335,24 +345,22 @@ def test_rpc_start(mocker, default_conf) -> None:
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock()
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_start()
assert not error
result = rpc._rpc_start()
assert '`Starting trader ...`' in result
assert freqtradebot.state == State.RUNNING
(error, result) = rpc.rpc_start()
assert error
result = rpc._rpc_start()
assert '*Status:* `already running`' in result
assert freqtradebot.state == State.RUNNING
@@ -363,134 +371,147 @@ def test_rpc_stop(mocker, default_conf) -> None:
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock()
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_stop()
assert not error
result = rpc._rpc_stop()
assert '`Stopping trader ...`' in result
assert freqtradebot.state == State.STOPPED
(error, result) = rpc.rpc_stop()
assert error
result = rpc._rpc_stop()
assert '*Status:* `already stopped`' in result
assert freqtradebot.state == State.STOPPED
def test_rpc_forcesell(default_conf, ticker, mocker) -> 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)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
cancel_order_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
cancel_order=cancel_order_mock,
get_order=MagicMock(
return_value={
'closed': True,
'type': 'LIMIT_BUY',
'status': 'closed',
'type': 'limit',
'side': 'buy'
}
)
),
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == '`trader is not running`'
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
rpc._rpc_forcesell(None)
freqtradebot.state = State.RUNNING
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == 'Invalid argument.'
with pytest.raises(RPCException, match=r'.*Invalid argument.*'):
rpc._rpc_forcesell(None)
(error, res) = rpc.rpc_forcesell('all')
assert not error
assert res == ''
rpc._rpc_forcesell('all')
freqtradebot.create_trade()
(error, res) = rpc.rpc_forcesell('all')
assert not error
assert res == ''
rpc._rpc_forcesell('all')
(error, res) = rpc.rpc_forcesell('1')
assert not error
assert res == ''
rpc._rpc_forcesell('1')
freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None)
assert error
assert res == '`trader is not running`'
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
rpc._rpc_forcesell(None)
(error, res) = rpc.rpc_forcesell('all')
assert error
assert res == '`trader is not running`'
with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
rpc._rpc_forcesell('all')
freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0
# make an limit-buy open trade
trade = Trade.query.filter(Trade.id == '1').first()
filled_amount = trade.amount / 2
mocker.patch(
'freqtrade.freqtradebot.exchange.get_order',
'freqtrade.exchange.Exchange.get_order',
return_value={
'closed': None,
'type': 'LIMIT_BUY'
'status': 'open',
'type': 'limit',
'side': 'buy',
'filled': filled_amount
}
)
# check that the trade is called, which is done
# by ensuring exchange.cancel_order is called
(error, res) = rpc.rpc_forcesell('1')
assert not error
assert res == ''
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
# and trade amount is updated
rpc._rpc_forcesell('1')
assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount
freqtradebot.create_trade()
trade = Trade.query.filter(Trade.id == '2').first()
amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch(
'freqtrade.exchange.Exchange.get_order',
return_value={
'status': 'open',
'type': 'limit',
'side': 'buy',
'filled': None
}
)
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
rpc._rpc_forcesell('2')
assert cancel_order_mock.call_count == 2
assert trade.amount == amount
freqtradebot.create_trade()
# make an limit-sell open trade
mocker.patch(
'freqtrade.freqtradebot.exchange.get_order',
'freqtrade.exchange.Exchange.get_order',
return_value={
'closed': None,
'type': 'LIMIT_SELL'
'status': 'open',
'type': 'limit',
'side': 'sell'
}
)
(error, res) = rpc.rpc_forcesell('2')
assert not error
assert res == ''
rpc._rpc_forcesell('3')
# status quo, no exchange calls
assert cancel_order_mock.call_count == 1
assert cancel_order_mock.call_count == 2
def test_performance_handle(default_conf, ticker, limit_buy_order,
limit_sell_order, mocker) -> None:
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, markets, mocker) -> None:
"""
Test rpc_performance() method
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
# Create some test data
@@ -506,39 +527,38 @@ def test_performance_handle(default_conf, ticker, limit_buy_order,
trade.close_date = datetime.utcnow()
trade.is_open = False
(error, res) = rpc.rpc_performance()
assert not error
res = rpc._rpc_performance()
assert len(res) == 1
assert res[0]['pair'] == 'BTC_ETH'
assert res[0]['pair'] == 'ETH/BTC'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 6.2)
def test_rpc_count(mocker, default_conf, ticker) -> 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)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
(error, trades) = rpc.rpc_count()
trades = rpc._rpc_count()
nb_trades = len(trades)
assert not error
assert nb_trades == 0
# Create some test data
freqtradebot.create_trade()
(error, trades) = rpc.rpc_count()
trades = rpc._rpc_count()
nb_trades = len(trades)
assert not error
assert nb_trades == 1

View File

@@ -7,49 +7,35 @@ from copy import deepcopy
from unittest.mock import MagicMock
from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.rpc.telegram import Telegram
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
def test_rpc_manager_object() -> None:
"""
Test the Arguments object has the mandatory methods
:return: None
"""
assert hasattr(RPCManager, '_init')
""" Test the Arguments object has the mandatory methods """
assert hasattr(RPCManager, 'send_msg')
assert hasattr(RPCManager, 'cleanup')
def test__init__(mocker, default_conf) -> None:
"""
Test __init__() method
"""
init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
""" Test __init__() method """
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
rpc_manager = RPCManager(freqtradebot)
assert rpc_manager.freqtrade == freqtradebot
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
assert init_mock.call_count == 1
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
"""
Test _init() method with Telegram disabled
"""
""" Test _init() method with Telegram disabled """
caplog.set_level(logging.DEBUG)
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
@@ -59,14 +45,12 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
len_modules = len(rpc_manager.registered_modules)
assert len_modules == 1
assert 'telegram' in rpc_manager.registered_modules
assert isinstance(rpc_manager.telegram, Telegram)
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
@@ -99,11 +83,11 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot)
# Check we have Telegram as a registered modules
assert 'telegram' in rpc_manager.registered_modules
assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
rpc_manager.cleanup()
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
assert 'telegram' not in rpc_manager.registered_modules
assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
assert telegram_mock.call_count == 1
@@ -120,7 +104,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples)
assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 0
@@ -135,5 +119,5 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples)
assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 1

View File

@@ -11,7 +11,6 @@ from datetime import datetime
from random import randint
from unittest.mock import MagicMock
from sqlalchemy import create_engine
from telegram import Update, Message, Chat
from telegram.error import NetworkError
@@ -21,7 +20,7 @@ from freqtrade.persistence import Trade
from freqtrade.rpc.telegram import Telegram
from freqtrade.rpc.telegram import authorized_only
from freqtrade.state import State
from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
from freqtrade.tests.conftest import get_patched_freqtradebot, patch_exchange, log_has
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
@@ -33,6 +32,9 @@ class DummyCls(Telegram):
super().__init__(freqtrade)
self.state = {'called': False}
def _init(self):
pass
@authorized_only
def dummy_handler(self, *args, **kwargs) -> None:
"""
@@ -61,9 +63,7 @@ def test__init__(default_conf, mocker) -> None:
def test_init(default_conf, mocker, caplog) -> None:
"""
Test _init() method
"""
""" Test _init() method """
start_polling = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
@@ -71,31 +71,16 @@ def test_init(default_conf, mocker, caplog) -> None:
assert start_polling.call_count == 0
# number of handles registered
assert start_polling.dispatcher.add_handler.call_count == 11
assert start_polling.dispatcher.add_handler.call_count > 0
assert start_polling.start_polling.call_count == 1
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
"['count'], ['help'], ['version']]"
"['count'], ['reload_conf'], ['help'], ['version']]"
assert log_has(message_str, caplog.record_tuples)
def test_init_disabled(default_conf, mocker, caplog) -> None:
"""
Test _init() method when Telegram is disabled
"""
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
Telegram(get_patched_freqtradebot(mocker, conf))
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
"['count'], ['help'], ['version']]"
assert not log_has(message_str, caplog.record_tuples)
def test_cleanup(default_conf, mocker) -> None:
"""
Test cleanup() method
@@ -104,51 +89,18 @@ def test_cleanup(default_conf, mocker) -> None:
updater_mock.stop = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
# not enabled
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
telegram.cleanup()
assert telegram._updater is None
assert updater_mock.call_count == 0
assert not hasattr(telegram._updater, 'stop')
assert updater_mock.stop.call_count == 0
# enabled
conf['telegram']['enabled'] = True
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
telegram.cleanup()
assert telegram._updater.stop.call_count == 1
def test_is_enabled(default_conf, mocker) -> None:
"""
Test is_enabled() method
"""
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
assert telegram.is_enabled()
def test_is_not_enabled(default_conf, mocker) -> None:
"""
Test is_enabled() method
"""
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
assert not telegram.is_enabled()
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)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
patch_exchange(mocker, None)
chat = Chat(0, 0)
update = Update(randint(1, 100))
@@ -156,7 +108,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
dummy = DummyCls(FreqtradeBot(conf))
dummy.dummy_handler(bot=MagicMock(), update=update)
assert dummy.state['called'] is True
assert log_has(
@@ -179,15 +131,14 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
patch_exchange(mocker, None)
chat = Chat(0xdeadbeef, 0)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
dummy = DummyCls(FreqtradeBot(conf))
dummy.dummy_handler(bot=MagicMock(), update=update)
assert dummy.state['called'] is False
assert not log_has(
@@ -210,14 +161,14 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
patch_exchange(mocker)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
dummy = DummyCls(FreqtradeBot(conf))
dummy.dummy_exception(bot=MagicMock(), update=update)
assert dummy.state['called'] is False
assert not log_has(
@@ -234,7 +185,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
)
def test_status(default_conf, update, mocker, ticker) -> None:
def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
"""
Test _status() method
"""
@@ -246,22 +197,25 @@ def test_status(default_conf, update, mocker, ticker) -> None:
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_pair_detail_url=MagicMock(),
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
status_table = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
rpc_trade_status=MagicMock(return_value=(False, [1, 2, 3])),
_rpc_trade_status=MagicMock(return_value=[1, 2, 3]),
_status_table=status_table,
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -277,16 +231,18 @@ def test_status(default_conf, update, mocker, ticker) -> None:
assert status_table.call_count == 1
def test_status_handle(default_conf, update, ticker, 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)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
status_table = MagicMock()
@@ -294,11 +250,11 @@ def test_status_handle(default_conf, update, ticker, mocker) -> None:
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_status_table=status_table,
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -319,32 +275,34 @@ def test_status_handle(default_conf, update, ticker, mocker) -> None:
telegram._status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0]
assert '[ETH/BTC]' in msg_mock.call_args_list[0][0][0]
def test_status_table_handle(default_conf, update, ticker, 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)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_order_id')
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
conf = deepcopy(default_conf)
conf['stake_amount'] = 15.0
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -369,12 +327,12 @@ def test_status_table_handle(default_conf, update, ticker, mocker) -> None:
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
assert int(fields[0]) == 1
assert fields[1] == 'BTC_ETH'
assert fields[1] == 'ETH/BTC'
assert msg_mock.call_count == 1
def test_daily_handle(default_conf, update, ticker, limit_buy_order,
limit_sell_order, mocker) -> None:
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
limit_sell_order, markets, mocker) -> None:
"""
Test _daily() method
"""
@@ -385,19 +343,21 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order,
return_value=15000.0
)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -453,7 +413,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
)
@@ -461,11 +421,11 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Try invalid data
@@ -484,8 +444,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0]
def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
limit_buy_order, limit_sell_order, mocker) -> None:
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test _profit() method
"""
@@ -493,19 +453,21 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
telegram._profit(bot=MagicMock(), update=update)
@@ -526,7 +488,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
msg_mock.reset_mock()
# Update the ticker with a market going up
mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', ticker_sell_up)
mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
@@ -541,49 +503,42 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
assert '*Best Performing:* `BTC_ETH: 6.20%`' in msg_mock.call_args_list[-1][0][0]
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
def test_telegram_balance_handle(default_conf, update, mocker) -> None:
"""
Test _balance() method
"""
mock_balance = [
{
'Currency': 'BTC',
'Balance': 10.0,
'Available': 12.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
mock_balance = {
'BTC': {
'total': 12.0,
'free': 12.0,
'used': 0.0
},
{
'Currency': 'ETH',
'Balance': 0.0,
'Available': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
'ETH': {
'total': 0.0,
'free': 0.0,
'used': 0.0
},
{
'Currency': 'USDT',
'Balance': 10000.0,
'Available': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
'USDT': {
'total': 10000.0,
'free': 10000.0,
'used': 0.0
},
{
'Currency': 'LTC',
'Balance': 10.0,
'Available': 10.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
'LTC': {
'total': 10.0,
'free': 10.0,
'used': 0.0
}
]
}
def mock_ticker(symbol, refresh):
"""
Mock Bittrex.get_ticker() response
"""
if symbol == 'USDT_BTC':
if symbol == 'BTC/USDT':
return {
'bid': 10000.00,
'ask': 10000.00,
@@ -598,29 +553,28 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value=mock_balance)
mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', side_effect=mock_ticker)
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance)
mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker)
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update)
result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1
assert '*Currency*: BTC' in result
assert '*Currency*: ETH' not in result
assert '*Currency*: USDT' in result
assert 'Balance' in result
assert 'Est. BTC' in result
assert '*BTC*: 12.00000000' in result
assert '*BTC:*' in result
assert '*ETH:*' not in result
assert '*USDT:*' in result
assert 'Balance:' in result
assert 'Est. BTC:' in result
assert 'BTC: 14.00000000' in result
def test_zero_balance_handle(default_conf, update, mocker) -> None:
@@ -628,18 +582,16 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
Test _balance() method when the Exchange platform returns nothing
"""
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value=[])
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={})
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update)
@@ -652,41 +604,35 @@ def test_start_handle(default_conf, update, mocker) -> None:
"""
Test _start() method
"""
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
assert freqtradebot.state == State.STOPPED
telegram._start(bot=MagicMock(), update=update)
assert freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 0
assert msg_mock.call_count == 1
def test_start_handle_already_running(default_conf, update, mocker) -> None:
"""
Test _start() method
"""
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.RUNNING
@@ -702,16 +648,14 @@ def test_stop_handle(default_conf, update, mocker) -> None:
Test _stop() method
"""
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.RUNNING
@@ -727,16 +671,14 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
Test _stop() method
"""
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -747,7 +689,29 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker) -> None:
def test_reload_conf_handle(default_conf, update, mocker) -> None:
""" Test _reload_conf() method """
patch_coinmarketcap(mocker)
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)
freqtradebot.state = State.RUNNING
assert freqtradebot.state == State.RUNNING
telegram._reload_conf(bot=MagicMock(), update=update)
assert freqtradebot.state == State.RELOAD_CONF
assert msg_mock.call_count == 1
assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
def test_forcesell_handle(default_conf, update, ticker, fee,
ticker_sell_up, markets, mocker) -> None:
"""
Test _forcesell() method
"""
@@ -757,12 +721,14 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker)
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -772,21 +738,22 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker)
assert trade
# Increase the price and sell it
mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', ticker_sell_up)
mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up)
update.message.text = '/forcesell 1'
telegram._forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 2
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker) -> None:
def test_forcesell_down_handle(default_conf, update, ticker, fee,
ticker_sell_down, markets, mocker) -> None:
"""
Test _forcesell() method
"""
@@ -796,12 +763,14 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -809,7 +778,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
# Decrease the price and sell it
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_down
)
@@ -822,14 +791,14 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
assert rpc_mock.call_count == 2
assert 'Selling' in rpc_mock.call_args_list[-1][0][0]
assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0]
assert 'Amount' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
def test_forcesell_all_handle(default_conf, update, ticker, mocker) -> None:
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _forcesell() method
"""
@@ -838,13 +807,16 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker) -> None:
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -873,11 +845,11 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Trader is not running
@@ -904,8 +876,8 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0]
def test_performance_handle(default_conf, update, ticker, limit_buy_order,
limit_sell_order, mocker) -> None:
def test_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, markets, mocker) -> None:
"""
Test _performance() method
"""
@@ -915,15 +887,17 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order,
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
get_markets=markets
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -942,7 +916,7 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order,
telegram._performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>BTC_ETH\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
assert '<code>ETH/BTC\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
def test_performance_handle_invalid(default_conf, update, mocker) -> None:
@@ -955,10 +929,10 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Trader is not running
@@ -968,7 +942,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
assert 'not running' in msg_mock.call_args_list[0][0][0]
def test_count_handle(default_conf, update, ticker, mocker) -> None:
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
"""
Test _count() method
"""
@@ -978,15 +952,17 @@ def test_count_handle(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_order_id')
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
get_markets=markets
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -1015,14 +991,14 @@ def test_help_handle(default_conf, update, mocker) -> None:
Test _help() method
"""
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram._help(bot=MagicMock(), update=update)
@@ -1035,14 +1011,13 @@ def test_version_handle(default_conf, update, mocker) -> None:
Test _version() method
"""
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
_send_msg=msg_mock
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
telegram._version(bot=MagicMock(), update=update)
@@ -1055,20 +1030,14 @@ def test_send_msg(default_conf, mocker) -> None:
Test send_msg() method
"""
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
conf = deepcopy(default_conf)
bot = MagicMock()
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, conf)
telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = False
telegram.send_msg('test', bot)
assert not bot.method_calls
bot.reset_mock()
telegram._config['telegram']['enabled'] = True
telegram.send_msg('test', bot)
telegram._send_msg('test', bot)
assert len(bot.method_calls) == 1
@@ -1077,16 +1046,15 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
Test send_msg() method
"""
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
conf = deepcopy(default_conf)
bot = MagicMock()
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
freqtradebot = get_patched_freqtradebot(mocker, conf)
telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = True
telegram.send_msg('test', bot)
telegram._send_msg('test', bot)
# Bot should've tried to send it twice
assert len(bot.method_calls) == 2

View File

@@ -9,7 +9,7 @@ from freqtrade.strategy.default_strategy import DefaultStrategy
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/BTC_ETH-1.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))
@@ -27,7 +27,7 @@ def test_default_strategy(result):
assert type(strategy.minimal_roi) is dict
assert type(strategy.stoploss) is float
assert type(strategy.ticker_interval) is int
assert type(strategy.ticker_interval) is str
indicators = strategy.populate_indicators(result)
assert type(indicators) is DataFrame
assert type(strategy.populate_buy_trend(indicators)) is DataFrame

View File

@@ -1,14 +1,39 @@
# pragma pylint: disable=missing-docstring, protected-access, C0103
import logging
import os
import pytest
from freqtrade.strategy import import_strategy
from freqtrade.strategy.default_strategy import DefaultStrategy
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.resolver import StrategyResolver
def test_import_strategy(caplog):
caplog.set_level(logging.DEBUG)
strategy = DefaultStrategy()
strategy.some_method = lambda *args, **kwargs: 42
assert strategy.__module__ == 'freqtrade.strategy.default_strategy'
assert strategy.some_method() == 42
imported_strategy = import_strategy(strategy)
assert dir(strategy) == dir(imported_strategy)
assert imported_strategy.__module__ == 'freqtrade.strategy'
assert imported_strategy.some_method() == 42
assert (
'freqtrade.strategy',
logging.DEBUG,
'Imported strategy freqtrade.strategy.default_strategy.DefaultStrategy '
'as freqtrade.strategy.DefaultStrategy',
) in caplog.record_tuples
def test_search_strategy():
default_location = os.path.join(os.path.dirname(
os.path.realpath(__file__)), '..', '..', 'strategy'
@@ -20,19 +45,21 @@ def test_search_strategy():
def test_load_strategy(result):
resolver = StrategyResolver()
resolver._load_strategy('TestStrategy')
resolver = StrategyResolver({'strategy': 'TestStrategy'})
assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in resolver.strategy.populate_indicators(result)
def test_load_strategy_custom_directory(result):
def test_load_strategy_invalid_directory(result, caplog):
resolver = StrategyResolver()
extra_dir = os.path.join('some', 'path')
with pytest.raises(
FileNotFoundError,
match=r".*No such file or directory: '{}'".format(extra_dir)):
resolver._load_strategy('TestStrategy', extra_dir)
resolver._load_strategy('TestStrategy', extra_dir)
assert (
'freqtrade.strategy.resolver',
logging.WARNING,
'Path "{}" does not exist'.format(extra_dir),
) in caplog.record_tuples
assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in resolver.strategy.populate_indicators(result)

View File

@@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring,C0103,protected-access
import freqtrade.tests.conftest as tt # test tools
from unittest.mock import MagicMock
# whitelist, blacklist, filtering, all of that will
# eventually become some rules to run on a generic ACL engine
@@ -12,118 +13,60 @@ def whitelist_conf():
config['stake_currency'] = 'BTC'
config['exchange']['pair_whitelist'] = [
'BTC_ETH',
'BTC_TKN',
'BTC_TRST',
'BTC_SWT',
'BTC_BCC'
'ETH/BTC',
'TKN/BTC',
'TRST/BTC',
'SWT/BTC',
'BCC/BTC'
]
config['exchange']['pair_blacklist'] = [
'BTC_BLK'
'BLK/BTC'
]
return config
def get_market_summaries():
return [{
'MarketName': 'BTC-TKN',
'High': 0.00000919,
'Low': 0.00000820,
'Volume': 74339.61396015,
'Last': 0.00000820,
'BaseVolume': 1664,
'TimeStamp': '2014-07-09T07:19:30.15',
'Bid': 0.00000820,
'Ask': 0.00000831,
'OpenBuyOrders': 15,
'OpenSellOrders': 15,
'PrevDay': 0.00000821,
'Created': '2014-03-20T06:00:00',
'DisplayMarketName': ''
}, {
'MarketName': 'BTC-ETH',
'High': 0.00000072,
'Low': 0.00000001,
'Volume': 166340678.42280999,
'Last': 0.00000005,
'BaseVolume': 42,
'TimeStamp': '2014-07-09T07:21:40.51',
'Bid': 0.00000004,
'Ask': 0.00000005,
'OpenBuyOrders': 18,
'OpenSellOrders': 18,
'PrevDay': 0.00000002,
'Created': '2014-05-30T07:57:49.637',
'DisplayMarketName': ''
}, {
'MarketName': 'BTC-BLK',
'High': 0.00000072,
'Low': 0.00000001,
'Volume': 166340678.42280999,
'Last': 0.00000005,
'BaseVolume': 3,
'TimeStamp': '2014-07-09T07:21:40.51',
'Bid': 0.00000004,
'Ask': 0.00000005,
'OpenBuyOrders': 18,
'OpenSellOrders': 18,
'PrevDay': 0.00000002,
'Created': '2014-05-30T07:57:49.637',
'DisplayMarketName': ''
}]
def get_health():
return [{'Currency': 'ETH', 'IsActive': True},
{'Currency': 'TKN', 'IsActive': True},
{'Currency': 'BLK', 'IsActive': True}]
def get_health_empty():
return []
def test_refresh_market_pair_not_in_whitelist(mocker):
def test_refresh_market_pair_not_in_whitelist(mocker, markets):
conf = whitelist_conf()
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
refreshedwhitelist = freqtradebot._refresh_whitelist(
conf['exchange']['pair_whitelist'] + ['BTC_XXX']
conf['exchange']['pair_whitelist'] + ['XXX/BTC']
)
# List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN']
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
assert whitelist == refreshedwhitelist
def test_refresh_whitelist(mocker):
def test_refresh_whitelist(mocker, markets):
conf = whitelist_conf()
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
# List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN']
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic(mocker):
def test_refresh_whitelist_dynamic(mocker, markets, tickers):
conf = whitelist_conf()
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
get_wallet_health=get_health,
get_market_summaries=get_market_summaries
'freqtrade.exchange.Exchange',
get_markets=markets,
get_tickers=tickers,
exchange_has=MagicMock(return_value=True)
)
# argument: use the whitelist dynamically by exchange-volume
whitelist = ['BTC_TKN', 'BTC_ETH']
whitelist = ['ETH/BTC', 'TKN/BTC']
refreshedwhitelist = freqtradebot._refresh_whitelist(
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
@@ -132,10 +75,10 @@ def test_refresh_whitelist_dynamic(mocker):
assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic_empty(mocker):
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty):
conf = whitelist_conf()
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health_empty)
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty)
# argument: use the whitelist dynamically by exchange-volume
whitelist = []

View File

@@ -13,7 +13,8 @@ from pandas import DataFrame
from freqtrade.analyze import Analyze, SignalType
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.tests.conftest import log_has
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'})
@@ -45,12 +46,12 @@ def test_analyze_object() -> None:
def test_dataframe_correct_length(result):
dataframe = Analyze.parse_ticker_dataframe(result)
assert len(result.index) == len(dataframe.index)
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
def test_dataframe_correct_columns(result):
assert result.columns.tolist() == \
['date', 'close', 'high', 'low', 'open', 'volume']
['date', 'open', 'high', 'low', 'close', 'volume']
def test_populates_buy_trend(result):
@@ -65,16 +66,16 @@ def test_populates_sell_trend(result):
assert 'sell' in dataframe.columns
def test_returns_latest_buy_signal(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
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('BTC-ETH', 5) == (True, False)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
@@ -82,11 +83,12 @@ def test_returns_latest_buy_signal(mocker):
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
def test_returns_latest_sell_signal(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
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(
@@ -94,7 +96,7 @@ def test_returns_latest_sell_signal(mocker):
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
@@ -102,45 +104,49 @@ def test_returns_latest_sell_signal(mocker):
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
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.analyze.get_ticker_history', return_value=None)
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
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.analyze.get_ticker_history', return_value=1)
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('foo', int(default_conf['ticker_interval']))
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.analyze.get_ticker_history', return_value=1)
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('xyz', int(default_conf['ticker_interval']))
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.analyze.get_ticker_history', return_value=1)
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}])
@@ -150,15 +156,16 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog):
return_value=DataFrame(ticks)
)
)
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
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):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
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(
@@ -166,20 +173,16 @@ def test_get_signal_handles_exceptions(mocker):
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, False)
assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv):
columns = ['date', 'close', 'high', 'low', 'open', 'volume']
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
# Test file without BV data
dataframe = Analyze.parse_ticker_dataframe(ticker_history_without_bv)
assert dataframe.columns.tolist() == columns
def test_tickerdata_to_dataframe(default_conf) -> None:
"""
@@ -187,8 +190,8 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
"""
analyze = Analyze(default_conf)
timerange = ((None, 'line'), None, -100)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
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['BTC_UNITEST']) == 100
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed

View File

@@ -9,7 +9,7 @@ import logging
import pytest
from freqtrade.arguments import Arguments
from freqtrade.arguments import Arguments, TimeRange
def test_arguments_object() -> None:
@@ -46,6 +46,11 @@ def test_parse_args_config() -> None:
assert args.config == '/dev/null'
def test_parse_args_db_url() -> None:
args = Arguments(['--db-url', 'sqlite:///test.sqlite'], '').get_parsed_arg()
assert args.db_url == 'sqlite:///test.sqlite'
def test_parse_args_verbose() -> None:
args = Arguments(['-v'], '').get_parsed_arg()
assert args.loglevel == logging.DEBUG
@@ -55,10 +60,10 @@ def test_parse_args_verbose() -> None:
def test_scripts_options() -> None:
arguments = Arguments(['-p', 'BTC_ETH'], '')
arguments = Arguments(['-p', 'ETH/BTC'], '')
arguments.scripts_options()
args = arguments.get_parsed_arg()
assert args.pair == 'BTC_ETH'
assert args.pair == 'ETH/BTC'
def test_parse_args_version() -> None:
@@ -107,8 +112,25 @@ def test_parse_args_dynamic_whitelist_invalid_values() -> None:
def test_parse_timerange_incorrect() -> None:
assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200')
assert (('line', None), 200, None) == Arguments.parse_timerange('200-')
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500')
assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-')
assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522')
timerange = Arguments.parse_timerange('20100522-20150730')
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
# Added test for unix timestamp - BTC genesis date
assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-')
assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000')
timerange = Arguments.parse_timerange('1231006505-1233360000')
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
# TODO: Find solution for the following case (passing timestamp in ms)
timerange = Arguments.parse_timerange('1231006505000-1233360000000')
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
Arguments.parse_timerange('-')
@@ -126,7 +148,7 @@ def test_parse_args_backtesting_custom() -> None:
'-c', 'test_conf.json',
'backtesting',
'--live',
'--ticker-interval', '1',
'--ticker-interval', '1m',
'--refresh-pairs-cached']
call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == 'test_conf.json'
@@ -134,7 +156,7 @@ def test_parse_args_backtesting_custom() -> None:
assert call_args.loglevel == logging.INFO
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 1
assert call_args.ticker_interval == '1m'
assert call_args.refresh_pairs is True
@@ -152,3 +174,19 @@ def test_parse_args_hyperopt_custom() -> None:
assert call_args.subparser == 'hyperopt'
assert call_args.spaces == ['buy']
assert call_args.func is not None
def test_testdata_dl_options() -> None:
args = [
'--pairs-file', 'file_with_pairs',
'--export', 'export/folder',
'--days', '30',
'--exchange', 'binance'
]
arguments = Arguments(args, '')
arguments.testdata_dl_options()
args = arguments.parse_args()
assert args.pairs_file == 'file_with_pairs'
assert args.export == 'export/folder'
assert args.days == 30
assert args.exchange == 'binance'

View File

@@ -6,13 +6,16 @@ Unit test file for configuration.py
import json
from copy import deepcopy
from unittest.mock import MagicMock
from argparse import Namespace
import pytest
from jsonschema import ValidationError
from freqtrade.arguments import Arguments
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
def test_configuration_object() -> None:
@@ -28,19 +31,19 @@ def test_configuration_object() -> None:
assert hasattr(Configuration, 'get_config')
def test_load_config_invalid_pair(default_conf, mocker) -> None:
def test_load_config_invalid_pair(default_conf) -> None:
"""
Test the configuration validator with an invalid PAIR format
"""
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'].append('BTC-ETH')
conf['exchange']['pair_whitelist'].append('ETH-BTC')
with pytest.raises(ValidationError, match=r'.*does not match.*'):
configuration = Configuration([])
configuration = Configuration(Namespace())
configuration._validate_config(conf)
def test_load_config_missing_attributes(default_conf, mocker) -> None:
def test_load_config_missing_attributes(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
@@ -48,7 +51,19 @@ def test_load_config_missing_attributes(default_conf, mocker) -> None:
conf.pop('exchange')
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
configuration = Configuration([])
configuration = Configuration(Namespace())
configuration._validate_config(conf)
def test_load_config_incorrect_stake_amount(default_conf) -> None:
"""
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\'.*'):
configuration = Configuration(Namespace())
configuration._validate_config(conf)
@@ -60,7 +75,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
read_data=json.dumps(default_conf)
))
configuration = Configuration([])
configuration = Configuration(Namespace())
validated_conf = configuration._load_config_file('somefile')
assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items()
@@ -68,7 +83,22 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
assert log_has('Validating configuration ...', caplog.record_tuples)
def test_load_config_file_exception(mocker, caplog) -> None:
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
"""
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(
read_data=json.dumps(conf)
))
Configuration(Namespace())._load_config_file('somefile')
assert file_mock.call_count == 1
assert log_has('Validating configuration ...', caplog.record_tuples)
def test_load_config_file_exception(mocker) -> None:
"""
Test Configuration._load_config_file() method
"""
@@ -76,14 +106,10 @@ def test_load_config_file_exception(mocker, caplog) -> None:
'freqtrade.configuration.open',
MagicMock(side_effect=FileNotFoundError('File not found'))
)
configuration = Configuration([])
configuration = Configuration(Namespace())
with pytest.raises(SystemExit):
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
configuration._load_config_file('somefile')
assert log_has(
'Config file "somefile" not found. Please create your config file',
caplog.record_tuples
)
def test_load_config(default_conf, mocker) -> None:
@@ -101,7 +127,6 @@ def test_load_config(default_conf, mocker) -> None:
assert validated_conf.get('strategy') == 'DefaultStrategy'
assert validated_conf.get('strategy_path') is None
assert 'dynamic_whitelist' not in validated_conf
assert 'dry_run_db' not in validated_conf
def test_load_config_with_params(default_conf, mocker) -> None:
@@ -112,13 +137,13 @@ def test_load_config_with_params(default_conf, mocker) -> None:
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--strategy-path', '/some/path',
'--dry-run-db',
'--db-url', 'sqlite:///someurl',
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
@@ -126,7 +151,44 @@ def test_load_config_with_params(default_conf, mocker) -> None:
assert validated_conf.get('dynamic_whitelist') == 10
assert validated_conf.get('strategy') == 'TestStrategy'
assert validated_conf.get('strategy_path') == '/some/path'
assert validated_conf.get('dry_run_db') is True
assert validated_conf.get('db_url') == 'sqlite:///someurl'
conf = default_conf.copy()
conf["dry_run"] = False
del conf["db_url"]
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--strategy-path', '/some/path'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('db_url') == DEFAULT_DB_PROD_URL
# Test dry=run with ProdURL
conf = default_conf.copy()
conf["dry_run"] = True
conf["db_url"] = DEFAULT_DB_PROD_URL
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--strategy-path', '/some/path'
]
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
assert validated_conf.get('db_url') == DEFAULT_DB_DRYRUN_URL
def test_load_custom_strategy(default_conf, mocker) -> None:
@@ -158,12 +220,12 @@ def test_show_info(default_conf, mocker, caplog) -> None:
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--dry-run-db'
'--db-url', 'sqlite:///tmp/testdb',
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
configuration.get_config()
@@ -175,23 +237,8 @@ def test_show_info(default_conf, mocker, caplog) -> None:
caplog.record_tuples
)
assert log_has(
'Parameter --dry-run-db detected ...',
caplog.record_tuples
)
assert log_has(
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite"',
caplog.record_tuples
)
# Test the Dry run condition
configuration.config.update({'dry_run': False})
configuration._load_common_config(configuration.config)
assert log_has(
'Dry run is disabled. (--dry_run_db ignored)',
caplog.record_tuples
)
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
assert log_has('Dry run is enabled', caplog.record_tuples)
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
@@ -202,13 +249,13 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'backtesting'
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
@@ -219,7 +266,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
@@ -246,12 +293,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1',
'--ticker-interval', '1m',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
@@ -259,7 +306,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--export', '/bar/foo'
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
@@ -270,13 +317,13 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
'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: 1 ...',
'Using ticker_interval: 1m ...',
caplog.record_tuples
)
@@ -310,14 +357,13 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'hyperopt',
'--epochs', '10',
'--use-mongodb',
'--spaces', 'all',
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
@@ -327,10 +373,32 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
assert log_has('Parameter --epochs detected ...', caplog.record_tuples)
assert log_has('Will run Hyperopt with for 10 epochs ...', caplog.record_tuples)
assert 'mongodb' in config
assert config['mongodb'] is True
assert log_has('Parameter --use-mongodb detected ...', caplog.record_tuples)
assert 'spaces' in config
assert config['spaces'] == ['all']
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
def test_check_exchange(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
configuration = Configuration(Namespace())
# Test a valid exchange
conf.get('exchange').update({'name': 'BITTREX'})
assert configuration.check_exchange(conf)
# Test a valid exchange
conf.get('exchange').update({'name': 'binance'})
assert configuration.check_exchange(conf)
# Test a invalid exchange
conf.get('exchange').update({'name': 'unknown_exchange'})
configuration.config = conf
with pytest.raises(
OperationalException,
match=r'.*Exchange "unknown_exchange" not supported.*'
):
configuration.check_exchange(conf)

View File

@@ -6,11 +6,11 @@ from freqtrade.analyze import Analyze
from freqtrade.optimize import load_data
from freqtrade.strategy.resolver import StrategyResolver
_pairs = ['BTC_ETH']
_pairs = ['ETH/BTC']
def load_dataframe_pair(pairs):
ld = load_data(None, ticker_interval=5, pairs=pairs)
ld = load_data(None, ticker_interval='5m', pairs=pairs)
assert isinstance(ld, dict)
assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]]

View File

@@ -6,7 +6,10 @@ from unittest.mock import MagicMock
import pytest
from requests.exceptions import RequestException
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
def test_pair_convertion_object():
@@ -37,7 +40,8 @@ def test_pair_convertion_object():
assert pair_convertion.price == 30000.123
def test_fiat_convert_is_supported():
def test_fiat_convert_is_supported(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._is_supported_fiat(fiat='USD') is True
assert fiat_convert._is_supported_fiat(fiat='usd') is True
@@ -45,7 +49,9 @@ def test_fiat_convert_is_supported():
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
def test_fiat_convert_add_pair():
def test_fiat_convert_add_pair(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
pair_len = len(fiat_convert._pairs)
@@ -67,18 +73,14 @@ def test_fiat_convert_add_pair():
def test_fiat_convert_find_price(mocker):
api_mock = MagicMock(return_value={
'price_usd': 12345.0,
'price_eur': 13000.2
})
mocker.patch('freqtrade.fiat_convert.Market.ticker', api_mock)
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
with pytest.raises(ValueError, match=r'The crypto symbol XRP is not supported.'):
fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD')
assert fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD') == 0.0
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0)
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
@@ -88,12 +90,17 @@ def test_fiat_convert_find_price(mocker):
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
def test_fiat_convert_unsupported_crypto(mocker, caplog):
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
def test_fiat_convert_get_price(mocker):
api_mock = MagicMock(return_value={
'price_usd': 28000.0,
'price_eur': 15000.0
})
mocker.patch('freqtrade.fiat_convert.Market.ticker', api_mock)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0)
fiat_convert = CryptoToFiatConverter()
@@ -124,12 +131,74 @@ def test_fiat_convert_get_price(mocker):
assert fiat_convert._pairs[0]._expiration is not expiration
def test_fiat_convert_without_network():
def test_fiat_convert_same_currencies(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0
def test_fiat_convert_two_FIAT(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0
def test_loadcryptomap(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert len(fiat_convert._cryptomap) == 2
assert fiat_convert._cryptomap["BTC"] == "1"
def test_fiat_init_network_exception(mocker):
# Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(side_effect=RequestException)
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
def test_fiat_convert_without_network(mocker):
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
cmc_temp = CryptoToFiatConverter._coinmarketcap
CryptoToFiatConverter._coinmarketcap = None
assert fiat_convert._coinmarketcap is None
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
CryptoToFiatConverter._coinmarketcap = cmc_temp
def test_convert_amount(mocker):
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
fiat_convert = CryptoToFiatConverter()
result = fiat_convert.convert_amount(
crypto_amount=1.23,
crypto_symbol="BTC",
fiat_symbol="USD"
)
assert result == 15184.35
result = fiat_convert.convert_amount(
crypto_amount=1.23,
crypto_symbol="BTC",
fiat_symbol="BTC"
)
assert result == 1.23

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,17 @@ Unit test file for main.py
"""
import logging
from copy import deepcopy
from unittest.mock import MagicMock
import pytest
from freqtrade.main import main, set_loggers
from freqtrade.tests.conftest import log_has
from freqtrade import OperationalException
from freqtrade.arguments import Arguments
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main, set_loggers, reconfigure
from freqtrade.state import State
from freqtrade.tests.conftest import log_has, patch_exchange
def test_parse_args_backtesting(mocker) -> None:
@@ -60,34 +65,153 @@ def test_set_loggers() -> None:
assert value2 is logging.INFO
def test_main(mocker, 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)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(
side_effect=KeyboardInterrupt
),
clean=MagicMock(),
worker=MagicMock(side_effect=Exception),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit) as pytest_wrapped_e:
main(args)
log_has('Starting freqtrade', caplog.record_tuples)
log_has('Got SIGINT, aborting ...', caplog.record_tuples)
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 42
# Test the BaseException case
mocker.patch(
'freqtrade.freqtradebot.FreqtradeBot.worker',
MagicMock(side_effect=BaseException)
)
with pytest.raises(SystemExit):
main(args)
log_has('Got fatal exception!', caplog.record_tuples)
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('Fatal exception!', caplog.record_tuples)
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)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=KeyboardInterrupt),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('SIGINT received, aborting ...', caplog.record_tuples)
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)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('Oh snap!', caplog.record_tuples)
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)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(return_value=State.RELOAD_CONF),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
# Raise exception as side effect to avoid endless loop
reconfigure_mock = mocker.patch(
'freqtrade.main.reconfigure', MagicMock(side_effect=Exception)
)
with pytest.raises(SystemExit):
main(['-c', 'config.json.example'])
assert reconfigure_mock.call_count == 1
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
def test_reconfigure(mocker, default_conf) -> None:
""" Test recreate() function """
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtrade = FreqtradeBot(default_conf)
# Renew mock to return modified data
conf = deepcopy(default_conf)
conf['stake_amount'] += 1
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: conf
)
# reconfigure should return a new instance
freqtrade2 = reconfigure(
freqtrade,
Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
)
# Verify we have a new instance with the new config
assert freqtrade is not freqtrade2
assert freqtrade.config['stake_amount'] + 1 == freqtrade2.config['stake_amount']

View File

@@ -9,7 +9,7 @@ from unittest.mock import MagicMock
from freqtrade.analyze import Analyze
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray,
common_datearray, file_dump_json)
common_datearray, file_dump_json, format_ms_time)
from freqtrade.optimize.__init__ import load_tickerdata_file
@@ -39,24 +39,24 @@ def test_datesarray_to_datetimearray(ticker_history):
assert dates[0].minute == 50
date_len = len(dates)
assert date_len == 3
assert date_len == 2
def test_common_datearray(default_conf, mocker) -> None:
def test_common_datearray(default_conf) -> None:
"""
Test common_datearray()
:return: None
"""
analyze = Analyze(default_conf)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerlist = {'BTC_UNITEST': tick}
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick}
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
dates = common_datearray(dataframes)
assert dates.size == dataframes['BTC_UNITEST']['date'].size
assert dates[0] == dataframes['BTC_UNITEST']['date'][0]
assert dates[-1] == dataframes['BTC_UNITEST']['date'][-1]
assert dates.size == dataframes['UNITTEST/BTC']['date'].size
assert dates[0] == dataframes['UNITTEST/BTC']['date'][0]
assert dates[-1] == dataframes['UNITTEST/BTC']['date'][-1]
def test_file_dump_json(mocker) -> None:
@@ -69,3 +69,25 @@ def test_file_dump_json(mocker) -> None:
file_dump_json('somefile', [1, 2, 3])
assert file_open.call_count == 1
assert json_dump.call_count == 1
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3], True)
assert file_open.call_count == 1
assert json_dump.call_count == 1
def test_format_ms_time() -> None:
"""
test format_ms_time()
:return: None
"""
# Date 2018-04-10 18:02:01
date_in_epoch_ms = 1523383321000
date = format_ms_time(date_in_epoch_ms)
assert type(date) is str
res = datetime.datetime(2018, 4, 10, 18, 2, 1, tzinfo=datetime.timezone.utc)
assert date == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
res = datetime.datetime(2017, 12, 13, 8, 2, 1, tzinfo=datetime.timezone.utc)
# Date 2017-12-13 08:02:01
date_in_epoch_ms = 1513152121000
assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')

View File

@@ -1,10 +1,11 @@
# pragma pylint: disable=missing-docstring, C0103
import os
from copy import deepcopy
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine
from freqtrade.exchange import Exchanges
from freqtrade import constants, OperationalException
from freqtrade.persistence import Trade, init, clean_dry_run_db
@@ -13,90 +14,60 @@ def init_persistence(default_conf):
init(default_conf)
def test_init_create_session(default_conf, mocker):
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
def test_init_create_session(default_conf):
# Check if init create a session
init(default_conf)
assert hasattr(Trade, 'session')
assert 'Session' in type(Trade.session).__name__
def test_init_dry_run_db(default_conf, mocker):
default_conf.update({'dry_run_db': True})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
def test_init_custom_db_url(default_conf, mocker):
conf = deepcopy(default_conf)
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
dry_run_db = 'tradesv3.dry_run.sqlite'
dry_run_db_swp = dry_run_db + '.swp'
# Update path to a value other than default, but still in-memory
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
if os.path.isfile(dry_run_db):
os.rename(dry_run_db, dry_run_db_swp)
# Check if the new tradesv3.dry_run.sqlite was created
init(default_conf)
assert os.path.isfile(dry_run_db) is True
# Delete the file made for this unitest and rollback to the previous
# tradesv3.dry_run.sqlite file
# 1. Delete file from the test
if os.path.isfile(dry_run_db):
os.remove(dry_run_db)
# 2. Rollback to the initial file
if os.path.isfile(dry_run_db_swp):
os.rename(dry_run_db_swp, dry_run_db)
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
def test_init_dry_run_without_db(default_conf, mocker):
default_conf.update({'dry_run_db': False})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
def test_init_invalid_db_url(default_conf):
conf = deepcopy(default_conf)
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
dry_run_db = 'tradesv3.dry_run.sqlite'
dry_run_db_swp = dry_run_db + '.swp'
if os.path.isfile(dry_run_db):
os.rename(dry_run_db, dry_run_db_swp)
# Check if the new tradesv3.dry_run.sqlite was created
init(default_conf)
assert os.path.isfile(dry_run_db) is False
# Rollback to the initial 'tradesv3.dry_run.sqlite' file
if os.path.isfile(dry_run_db_swp):
os.rename(dry_run_db_swp, dry_run_db)
# Update path to a value other than default, but still in-memory
conf.update({'db_url': 'unknown:///some.url'})
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init(conf)
def test_init_prod_db(default_conf, mocker):
default_conf.update({'dry_run': False})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
conf = deepcopy(default_conf)
conf.update({'dry_run': False})
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
# First, protect the existing 'tradesv3.sqlite' (Do not delete user data)
prod_db = 'tradesv3.sqlite'
prod_db_swp = prod_db + '.swp'
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
if os.path.isfile(prod_db):
os.rename(prod_db, prod_db_swp)
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
# Check if the new tradesv3.sqlite was created
init(default_conf)
assert os.path.isfile(prod_db) is True
# Delete the file made for this unitest and rollback to the previous tradesv3.sqlite file
def test_init_dryrun_db(default_conf, mocker):
conf = deepcopy(default_conf)
conf.update({'dry_run': True})
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
# 1. Delete file from the test
if os.path.isfile(prod_db):
os.remove(prod_db)
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
# Rollback to the initial 'tradesv3.sqlite' file
if os.path.isfile(prod_db_swp):
os.rename(prod_db_swp, prod_db)
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
@pytest.mark.usefixtures("init_persistence")
def test_update_with_bittrex(limit_buy_order, limit_sell_order):
def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee):
"""
On this test we will buy and sell a crypto currency.
@@ -125,10 +96,11 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order):
"""
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
assert trade.open_order_id is None
assert trade.open_rate is None
@@ -151,12 +123,13 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'something'
@@ -174,12 +147,13 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price_exception(limit_buy_order):
def test_calc_close_trade_price_exception(limit_buy_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'something'
@@ -190,10 +164,11 @@ def test_calc_close_trade_price_exception(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_update_open_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
)
assert trade.open_order_id is None
@@ -201,7 +176,7 @@ def test_update_open_order(limit_buy_order):
assert trade.close_profit is None
assert trade.close_date is None
limit_buy_order['closed'] = False
limit_buy_order['status'] = 'open'
trade.update(limit_buy_order)
assert trade.open_order_id is None
@@ -213,10 +188,11 @@ def test_update_open_order(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_update_invalid_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
)
limit_buy_order['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'):
@@ -224,12 +200,13 @@ def test_update_invalid_order(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_trade_price(limit_buy_order):
def test_calc_open_trade_price(limit_buy_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'open_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
@@ -242,12 +219,13 @@ def test_calc_open_trade_price(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'close_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
@@ -264,12 +242,13 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit(limit_buy_order, limit_sell_order):
def test_calc_profit(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
@@ -295,12 +274,13 @@ def test_calc_profit(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
@@ -319,40 +299,43 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order):
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
def test_clean_dry_run_db(default_conf):
init(default_conf, create_engine('sqlite://'))
def test_clean_dry_run_db(default_conf, fee):
init(default_conf)
# Simulate dry_run entries
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
amount=123.0,
fee=0.0025,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='BITTREX',
exchange='bittrex',
open_order_id='dry_run_buy_12345'
)
Trade.session.add(trade)
trade = Trade(
pair='BTC_ETC',
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee=0.0025,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='BITTREX',
exchange='bittrex',
open_order_id='dry_run_sell_12345'
)
Trade.session.add(trade)
# Simulate prod entry
trade = Trade(
pair='BTC_ETC',
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee=0.0025,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='BITTREX',
exchange='bittrex',
open_order_id='prod_buy_12345'
)
Trade.session.add(trade)
@@ -364,3 +347,109 @@ def test_clean_dry_run_db(default_conf):
# We have now only the prod
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
def test_migrate_old(mocker, default_conf, fee):
"""
Test Database migration(starting with old pairformat)
"""
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 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_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_ETC', 1, {fee},
0.00258580, {stake}, {amount},
'2017-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 == "bittrex"
def test_migrate_new(mocker, default_conf, fee):
"""
Test Database migration (starting with new pairformat)
"""
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 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_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETC/BTC', 1, {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"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
[
{"O": 0.00162008, "H": 0.00162008, "L": 0.00162008, "C": 0.00162008, "V": 108.14853839, "T": "2017-11-04T23:02:00", "BV": 0.17520927}
]

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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