Compare commits

...

430 Commits

Author SHA1 Message Date
Samuel Husso
20ebd744c3 Freqtrade 0.16.0 release 2018-05-12 09:43:22 +03:00
Gérald LONLAS
70f2aed0a7 Merge pull request #375 from gcarq/update_version
Update freqtrade version
2018-01-12 23:21:06 -08:00
Gerald Lonlas
3087ca0823 Update freqtrade version 2018-01-12 22:56:39 -08:00
Janne Sinivirta
372dc5b49a Merge pull request #368 from gcarq/pyup-update-pymarketcap-3.3.145-to-3.3.147
Update pymarketcap to 3.3.147
2018-01-13 07:33:16 +02:00
Janne Sinivirta
030aedc7d4 Merge pull request #362 from gcarq/pyup-update-ta-lib-0.4.14-to-0.4.15
Update ta-lib to 0.4.15
2018-01-13 07:33:04 +02:00
pyup-bot
a26cb4bc6b Update pymarketcap from 3.3.145 to 3.3.147 2018-01-12 11:08:23 +01:00
Gérald LONLAS
1fe86656e1 Merge pull request #364 from gcarq/fix/issue-363
Fix plot_dataframe.py
2018-01-11 21:26:10 -08:00
Gerald Lonlas
39c6e5263a Fix plot_dataframe.py 2018-01-11 21:09:04 -08:00
pyup-bot
46a1a2de10 Update ta-lib from 0.4.14 to 0.4.15 2018-01-11 20:53:26 +01:00
Samuel Husso
3a902289f1 testdata path to use os.path.join (#360) 2018-01-11 12:58:06 +01:00
Samuel Husso
3ac3ead2cf Merge pull request #358 from ermakus/set_requests_default_timeout
Set timeout for bittrex only
2018-01-11 08:51:21 +02:00
Anton Ermak
0d0737d1f6 Resolve conflict 2018-01-11 13:36:56 +07:00
Samuel Husso
27fcf62011 Merge pull request #354 from gcarq/linter-fixes
Linter fixes
2018-01-11 08:32:48 +02:00
Anton Ermak
bb91fdbaf9 oops, print removed 2018-01-11 13:26:49 +07:00
Anton Ermak
11cbb9188b Set timeout for bittrex only 2018-01-11 12:24:05 +07:00
Janne Sinivirta
c11102cf4a another run of autopep8 2018-01-11 07:08:56 +02:00
Janne Sinivirta
02fcbbb6d2 few flake8 fixes 2018-01-11 07:08:56 +02:00
Janne Sinivirta
0d6051e6f9 formatting 2018-01-11 07:08:56 +02:00
Janne Sinivirta
6a433282dc fix literal comparison 2018-01-11 07:08:56 +02:00
Janne Sinivirta
8fb404b0f8 ignore talib.abstract in pylint 2018-01-11 07:08:56 +02:00
Janne Sinivirta
64530c6196 remove unused variables 2018-01-11 07:08:56 +02:00
Janne Sinivirta
86db6c9084 sort imports 2018-01-11 07:08:56 +02:00
Janne Sinivirta
0abc30401c linter fixes and cleanups 2018-01-11 06:50:36 +02:00
Janne Sinivirta
1b6b0ad9d2 autopep8 2018-01-11 06:50:36 +02:00
Janne Sinivirta
7cdbd550c8 Merge pull request #351 from gcarq/feat/hyperopt-resume
resume hyperopt run
2018-01-11 06:47:05 +02:00
Samuel Husso
69f68c428e Merge pull request #355 from ermakus/set_requests_default_timeout
Set requests default timeout
2018-01-10 14:22:39 +02:00
Anton Ermak
abcdbcfd39 Set requests default timeout 2018-01-10 17:37:49 +07:00
Samuel Husso
e67c652988 use os.path.join, fix docstrings 2018-01-10 11:50:00 +02:00
Gérald LONLAS
ddc711ec93 Merge pull request #353 from kryofly/test_exchange_bittrex
test: increase coverage of exchange.bittrex
2018-01-09 17:26:38 -08:00
kryofly
b9bf5c1118 test: increase coverage of exchange.bittrex 2018-01-09 14:07:50 +01:00
Robert Moggach
9840e0b5b8 use HTTPS git URL in README.md (#347) 2018-01-09 13:31:59 +01:00
Samuel Husso
ffae0b2cd5 hyperopt: prettyfie best values when receiving SIGINT, use the global TRIALS 2018-01-09 12:37:56 +02:00
Samuel Husso
fe2b0c2862 add unittest to save and read trials file 2018-01-09 12:26:52 +02:00
Samuel Husso
1647e7a0c1 update fix failing tests, unitest that resume hyperopt functionality works 2018-01-09 12:26:52 +02:00
Samuel Husso
b35fa4c9f6 hyperopt: show the best results so far 2018-01-09 12:25:58 +02:00
Samuel Husso
a48840509b Hyperopt: use results from previous runs 2018-01-09 12:25:58 +02:00
Samuel Husso
ca8cab0ce9 Hyperopt to handle SIGINT by saving/reading the trials file 2018-01-09 12:25:58 +02:00
Gérald LONLAS
bbcf6943ce Merge pull request #349 from gcarq/docs-update
Update installation.md
2018-01-08 23:50:21 -08:00
Samuel Husso
fbf9bfe897 Update installation.md
it seems that ta-lib requires python3.6-dev package to be installed
2018-01-09 07:24:00 +02:00
Janne Sinivirta
e46fcf0e02 Merge pull request #344 from gcarq/fix-hyperopt-stoploss
Fix hyperopt stoploss
2018-01-09 06:42:13 +02:00
Janne Sinivirta
f7dd5e6396 use sensible value for stoploss in test 2018-01-08 22:00:10 +02:00
Janne Sinivirta
dd2ccea6e5 fix wrong range in stoploss search space 2018-01-08 21:59:46 +02:00
Janne Sinivirta
3d13eb2dc2 Merge pull request #342 from stephendade/fiatfix
Added missing fiat currencies to config
2018-01-08 10:11:01 +02:00
Stephen Dade
26b8661325 Added missing fiat currencies to config 2018-01-08 18:51:04 +11:00
Janne Sinivirta
fa97a82568 Merge pull request #332 from gcarq/hyperopt_stoploss
Add stoploss to the hyperopt parameters
2018-01-08 08:03:09 +02:00
Janne Sinivirta
1ae73d7da2 Merge branch 'develop' into hyperopt_stoploss 2018-01-08 07:49:44 +02:00
Samuel Husso
d8e692c9a3 Merge pull request #339 from gcarq/upgrade_flake8
Upgrade flake8
2018-01-08 07:34:45 +02:00
Gerald Lonlas
ca05d1f79e Fix for flake8 2018-01-07 21:08:12 -08:00
Janne Sinivirta
9dd38aebe0 add stoploss to the hyperopt parameters 2018-01-07 21:08:12 -08:00
Gérald LONLAS
ceded8a20a Merge pull request #338 from gcarq/fix/issue-337
Fix hypeopt issue when no result found
2018-01-07 21:07:07 -08:00
Gerald Lonlas
9c21077dc1 Fix hypeopt issue when no result found 2018-01-07 17:53:21 -08:00
Gérald LONLAS
fca6a09a41 Merge pull request #293 from jblestang/fix_issue_278
The /status table command was getting slower when we had multiple trades opened
2018-01-07 15:15:25 -08:00
Jean-Baptiste LE STANG
bba711c89a with flake8 ... 2018-01-07 23:35:16 +01:00
Jean-Baptiste LE STANG
5fbaa6d4cf rebasing for ta-lib dependency 2018-01-07 23:30:37 +01:00
Jean-Baptiste LE STANG
5b1f84f816 without debug print 2018-01-07 23:29:19 +01:00
Jean-Baptiste LE STANG
65127533ef fixing unittest 2018-01-07 23:29:19 +01:00
Jean-Baptiste LE STANG
05ca00b623 Add a unitest and fix pep8 2018-01-07 23:26:45 +01:00
Jean-Baptiste LE STANG
4b6d855e63 fix a typo in the description of get_ticker 2018-01-07 23:26:45 +01:00
Jean-Baptiste LE STANG
7d7752efbf really fixing 2018-01-07 23:26:45 +01:00
Jean-Baptiste LE STANG
ce6f6ab9fe fixing refresh argument ... 2018-01-07 23:26:45 +01:00
Jean-Baptiste LE STANG
3a0569cfd3 force refresh is the value has never been set 2018-01-07 23:26:45 +01:00
Jean-Baptiste LE STANG
7d21015b52 get_ticker can return a cached value 2018-01-07 23:26:45 +01:00
Gérald LONLAS
a57707071c Merge pull request #334 from gcarq/pyup-update-ta-lib-0.4.10-to-0.4.14
Update ta-lib to 0.4.14
2018-01-07 14:25:01 -08:00
Gérald LONLAS
2a347e4027 Merge pull request #328 from kryofly/datadir
--datadir <path> argument
2018-01-07 14:17:43 -08:00
Jean-Baptiste LE STANG
4c8ae3a7af without debug print 2018-01-07 23:15:33 +01:00
Jean-Baptiste LE STANG
2773ce7ebf rebasing against develop 2018-01-07 21:34:42 +01:00
Jean-Baptiste LE STANG
f4e4104d14 Fixing unitest 2018-01-07 21:26:43 +01:00
Jean-Baptiste LE STANG
b722a89276 fixing unittest 2018-01-07 21:24:17 +01:00
pyup-bot
4bf6711dbb Update ta-lib from 0.4.10 to 0.4.14 2018-01-07 18:08:15 +01:00
Janne Sinivirta
5be733a174 fix flake8 warnings 2018-01-07 14:37:09 +02:00
Janne Sinivirta
c3cae5dfc4 have pip upgrade flake8 and coveralls 2018-01-07 14:32:01 +02:00
kryofly
0c9d862a49 docs: --datadir documentation 2018-01-07 10:15:26 +01:00
Jean-Baptiste LE STANG
975a785e68 Add a unitest and fix pep8 2018-01-07 10:14:11 +01:00
Jean-Baptiste LE STANG
6be607e528 fix a typo in the description of get_ticker 2018-01-07 10:14:11 +01:00
Jean-Baptiste LE STANG
80c4dea875 really fixing 2018-01-07 10:14:11 +01:00
Jean-Baptiste LE STANG
9e7a4c3717 fixing refresh argument ... 2018-01-07 10:14:11 +01:00
Jean-Baptiste LE STANG
c72e9c3cef force refresh is the value has never been set 2018-01-07 10:14:11 +01:00
Jean-Baptiste LE STANG
8175eaa48a get_ticker can return a cached value 2018-01-07 10:14:11 +01:00
kryofly
890083ce7f Merge branch 'develop' into datadir 2018-01-07 10:00:35 +01:00
Gérald LONLAS
454cd16df4 Merge pull request #331 from gcarq/fix/work_without_network
Fix _coinmarketcap that fails backtesting and Hyperopt when no network
2018-01-06 21:33:24 -08:00
Gérald LONLAS
7e233b536c Merge pull request #323 from gcarq/add_indicators
Add 28 optional indicators populate_indicators()
2018-01-06 21:30:27 -08:00
Gérald LONLAS
ae19ab3dd3 Merge pull request #330 from gcarq/feature/better_hp_result_display
Make readable hyperopt best parameters result
2018-01-06 21:30:02 -08:00
Gerald Lonlas
bf4b2dc05e Fix _coinmarketcap that fails backtesting and Hyperopt when no network 2018-01-06 21:21:28 -08:00
Janne Sinivirta
571ea6a2bc Merge pull request #329 from gcarq/pyup-update-numpy-1.13.3-to-1.14.0
Update numpy to 1.14.0
2018-01-07 07:19:29 +02:00
Gerald Lonlas
b3ea0f4ec5 Make readable hyperopt best parameters result 2018-01-06 17:19:48 -08:00
pyup-bot
d4c8ad5ba7 Update numpy from 1.13.3 to 1.14.0 2018-01-07 01:47:18 +01:00
Gérald LONLAS
2432c9f290 Merge pull request #324 from kryofly/parse-common
Parsing: common options, reduce function scope
2018-01-06 15:11:30 -08:00
Gérald LONLAS
7f7d53adb7 Merge pull request #327 from gcarq/fix_profit_experimental
Fix profit experimental
2018-01-06 15:05:20 -08:00
kryofly
60ed4b9d1e --datadir <path> argument
This argument enables usage of different backtesting directories.
Useful if one wants compare backtesting performance over time.
2018-01-06 23:24:35 +01:00
Gerald Lonlas
83a999d16e Change Bollinger bands for qtpylib.bollinger_bands 2018-01-06 13:19:45 -08:00
Janne Sinivirta
a29f3de025 fix variable names to pythonic 2018-01-06 21:21:56 +02:00
Janne Sinivirta
6ab0ec6aac only apply profit guarantee to sell_signal 2018-01-06 21:18:57 +02:00
kryofly
984204e380 let parse_args only parse, no continuation
This removes parse_args() from the call stack
It pushes down the test-mocking one level [from parse_args() to main()].
Moves parse_args into a more generic 'modules' parsing direction.
2018-01-06 11:21:09 +01:00
Gerald Lonlas
297166fcb9 Add 29 optional indicators populate_indicators() 2018-01-06 01:11:01 -08:00
kryofly
e6e57e47cf plot script can take arguments 2018-01-06 09:55:15 +01:00
Janne Sinivirta
bcde377019 Merge pull request #321 from gcarq/log-exceptions
Log exceptions
2018-01-06 10:14:57 +02:00
Samuel Husso
2d39759d34 pep8 fix 2018-01-06 10:08:25 +02:00
kryofly
e4500af736 test case for common CLI parsing
Rearrange current tests.
2018-01-06 08:27:44 +01:00
Janne Sinivirta
41933c31ca Merge pull request #315 from kryofly/tests_jan05
tests cover more backtesting
2018-01-06 09:26:20 +02:00
kryofly
47675943ee split common command line args parsing
A new function parse_args_common() that only parses
common command line options. The returned object can
be composed to parse more arguments.
As is done by parse_args().
2018-01-06 07:39:05 +01:00
Gérald LONLAS
74a708b794 Merge pull request #312 from gcarq/fix_backtesting_header
Fix Backtesting header alignment
2018-01-05 19:30:04 -08:00
Janne Sinivirta
833c7f21af Merge pull request #306 from stephendade/timeoutfix
Unfilled order timeouts - now using timestamps from exchange
2018-01-05 18:04:27 +02:00
Janne Sinivirta
f8eedc69dd Merge pull request #313 from seansan/patch-4
Add CCI
2018-01-05 18:04:08 +02:00
Samuel Husso
797324c35e Merge pull request #317 from gcarq/pyup-update-pymarketcap-3.3.143-to-3.3.145
Update pymarketcap to 3.3.145
2018-01-05 13:48:51 +02:00
Samuel Husso
ae967a4f40 add test to handle analyze_ticker raising exception 2018-01-05 13:43:56 +02:00
pyup-bot
188fc69e56 Update pymarketcap from 3.3.143 to 3.3.145 2018-01-05 12:08:16 +01:00
Samuel Husso
be8506b45e log exceptions, catch *all* exceptions when analysing ticker 2018-01-05 12:18:44 +02:00
kryofly
79fcd0b06c tests cover more backtesting 2018-01-05 10:44:10 +01:00
kryofly
421ccb23d3 split load tickerdata function 2018-01-05 10:20:48 +01:00
seansan
f1969175cd Add CCI 2018-01-05 08:40:03 +01:00
Gerald Lonlas
7fd6d089c0 Fix Backtesting header alignment 2018-01-04 23:14:10 -08:00
Gérald LONLAS
552fba773d Merge pull request #310 from gcarq/pyup-update-pytest-3.3.1-to-3.3.2
Update pytest to 3.3.2
2018-01-04 22:38:37 -08:00
Gérald LONLAS
8e272cfd53 Merge pull request #311 from gcarq/use_named_arguments
Use named argument for backtest()
2018-01-04 22:38:25 -08:00
Gérald LONLAS
36fbe54634 Merge pull request #307 from gcarq/pyup-update-pymarketcap-3.3.141-to-3.3.143
Update pymarketcap to 3.3.143
2018-01-04 22:38:04 -08:00
Gerald Lonlas
90017998fc Use named argument for backtest() 2018-01-04 22:27:55 -08:00
Stephen Dade
ebe95ba1e1 Open order times should be strings, not datetime objectsy 2018-01-05 15:12:13 +11:00
pyup-bot
c803762704 Update pytest from 3.3.1 to 3.3.2 2018-01-05 01:28:53 +01:00
pyup-bot
f8d8f3347a Update pymarketcap from 3.3.141 to 3.3.143 2018-01-04 20:08:11 +01:00
Stephen Dade
d4fcc38a57 Unfilled order timeouts - now using timestamps from exchange 2018-01-05 01:39:01 +11:00
Janne Sinivirta
c60ef181dc Merge pull request #297 from jblestang/add_stoploss_and_use_sell_profit_only_to_hyperopt
Add stoploss, sell_only_profit and use_sell_signal conf parameters to backtest function
2018-01-04 13:33:01 +02:00
Samuel Husso
db4ad2f6f9 Merge pull request #295 from stephendade/Ordertimeout
Added order timeout handling
2018-01-04 09:26:16 +02:00
Stephen Dade
b5d2cfecc7 Unfilled Order timeout - better documentation and variable naming 2018-01-04 10:35:57 +11:00
Jean-Baptiste LE STANG
75955fcc04 Add a unitest and fix pep8 2018-01-03 17:58:08 +01:00
Jean-Baptiste LE STANG
050e73d960 fix a typo in the description of get_ticker 2018-01-03 17:51:01 +01:00
Jean-Baptiste LE STANG
0f2d3adbbc applying pep8 2018-01-03 17:36:40 +01:00
Jean-Baptiste LE STANG
ea6a1c629d fixing pep8 compliance 2018-01-03 11:50:30 +01:00
Jean-Baptiste LE STANG
eb53a796e2 pep8 compliance 2018-01-03 11:35:54 +01:00
Jean-Baptiste LE STANG
2d273a8509 Update unittests 2018-01-03 11:30:24 +01:00
Stephen Dade
7169ad557f Correct documentation for opentradetimeout 2018-01-03 21:24:42 +11:00
Stephen Dade
b4d6250d55 Added order timeout handling 2018-01-03 21:22:35 +11:00
Jean-Baptiste LE STANG
45f2d01895 - add a profit/loss counter
- the use of the sell_signal is conditional now (taken from the config)
2018-01-03 11:19:46 +01:00
Jean-Baptiste LE STANG
c176ace889 Adding sell_profit_only and stoploss in hyperopt 2018-01-03 10:56:18 +01:00
Gérald LONLAS
1ce4613aad Merge pull request #296 from gcarq/update_documentation
Update documentation
2018-01-03 00:07:41 -08:00
Gerald Lonlas
eb473842b8 Update documentation 2018-01-02 23:59:14 -08:00
Gérald LONLAS
407eaa0870 Merge pull request #279 from gcarq/revamp_documentations
Reorder and revamp the documentation
2018-01-02 23:48:49 -08:00
Gérald LONLAS
9b09b5aa29 Merge pull request #291 from gcarq/backtesting_speed_opt
Backtesting speed optimizations
2018-01-02 23:35:47 -08:00
Gerald Lonlas
70d1511f73 Update ISSUE_TEMPLATE.md and PULL_REQUEST_TEMPLATE.md 2018-01-02 23:34:26 -08:00
Gérald LONLAS
4a717f3df8 Merge pull request #294 from jblestang/add_trades_count_in_performance
Add trades count foreach pair in performance command
2018-01-02 23:03:30 -08:00
Gerald Lonlas
cb7c36a512 Add Backtesting and Hyperopt documentation 2018-01-02 22:50:54 -08:00
Gerald Lonlas
f37c495b90 Update the documentation from the PR review 2018-01-02 22:50:54 -08:00
Gerald Lonlas
284c6c4223 Reorder and revamp the documentation 2018-01-02 22:50:54 -08:00
Samuel Husso
fd5497cfc7 Merge pull request #265 from gcarq/feature/experimental/force_profit_sell
Add experimental feature to sell only if we make a profit
2018-01-03 08:14:54 +02:00
Samuel Husso
208d3770da Merge pull request #292 from jblestang/fix_pair_black_list
Bug in blacklist pair handling
2018-01-03 07:54:18 +02:00
Jean-Baptiste LE STANG
01b49dc502 Merge branch 'develop' into add_trades_count_in_performance 2018-01-03 00:06:56 +01:00
Jean-Baptiste LE STANG
fbb19e451d Adding the number of trades for each traded pair in the performance command 2018-01-03 00:06:50 +01:00
Jean-Baptiste LE STANG
a1ffa4497d Merge branch 'develop' into fix_issue_278 2018-01-02 23:12:21 +01:00
Jean-Baptiste LE STANG
e69f9dd029 Bad unittest detected reading coverage report, rewritten and bug found 2018-01-02 23:00:03 +01:00
Janne Sinivirta
fed3024302 rewrite get_timeframe in backtesting 2018-01-02 21:54:31 +02:00
Janne Sinivirta
dc2f048c98 make tuples smaller in backtesting loops 2018-01-02 21:52:47 +02:00
Samuel Husso
f4ccd4609b Merge pull request #284 from jblestang/fix_issue_283
fixing the sorting issue in MarketSummary when using --dynamic-whitelist (issue #283)
2018-01-02 21:00:20 +02:00
Samuel Husso
1e3a29c049 Merge pull request #287 from gcarq/fix_tabulate
Improve backtesting result formatting
2018-01-02 19:00:54 +02:00
Janne Sinivirta
82e9ed2ac2 shorten table title to match table length 2018-01-02 17:53:47 +02:00
Janne Sinivirta
ae52880f81 improve backtesting result formatting 2018-01-02 17:39:02 +02:00
Jean-Baptiste LE STANG
90236fb537 Fixing error log on inactive wallet 2018-01-02 15:17:23 +01:00
Jean-Baptiste LE STANG
55d0d27756 message too long, removing URL for now 2018-01-02 14:55:31 +01:00
Jean-Baptiste LE STANG
d849694a70 Adding URL to market graph and number of trades/pair in /performance commande 2018-01-02 14:43:38 +01:00
Jean-Baptiste LE STANG
29987c3ff6 Adding the number of trades in the performance display 2018-01-02 14:32:13 +01:00
Jean-Baptiste LE STANG
5f696a0cce really fixing 2018-01-02 14:13:55 +01:00
Jean-Baptiste LE STANG
90d3c09536 fixing refresh argument ... 2018-01-02 14:13:40 +01:00
Jean-Baptiste LE STANG
3f65fc014e flake8 on tests 2018-01-02 13:46:16 +01:00
Jean-Baptiste LE STANG
5344b711ea Add two more unit tests for covering pair that are in a blacklist, and unknown pairs in the conf 2018-01-02 13:42:10 +01:00
Jean-Baptiste LE STANG
a3e827c144 with flake8 code review 2018-01-02 12:18:26 +01:00
Jean-Baptiste LE STANG
52e267e864 fix for issue #283 2018-01-02 12:04:47 +01:00
Jean-Baptiste LE STANG
165781a545 force refresh is the value has never been set 2018-01-02 11:00:22 +01:00
Jean-Baptiste LE STANG
e10a3d1f9d get_ticker can return a cached value 2018-01-02 10:56:42 +01:00
Samuel Husso
0c11d4443f Merge pull request #277 from stephendade/patch-1
Fixed pytest typo
2018-01-02 07:47:23 +02:00
Stephen
50be2fabbf Fixed pytest typo 2018-01-02 15:04:41 +11:00
jblestang
7a2e9ef535 Add fiat display in sell msg (#271)
* Display amount (fiat currency) in the sell message
* Display also base currency
* Adding more info in Buy Message, the stake amount, and the amount using FIAT Converter
* fix display style and width
* Fixing flake8
2018-01-01 14:21:43 -08:00
Gérald LONLAS
079f2e3609 Merge pull request #276 from jblestang/issue-273
Removing tilde and change profit to loss when negative profit is made
2018-01-01 14:19:29 -08:00
Jean-Baptiste LE STANG
0e0d613191 Removing tilde and change profit to loss when negative profit is made 2018-01-01 20:18:38 +01:00
Samuel Husso
de68209f3b Revert "Make get_signals async. This should speed up create_trade calls by at least 10x. (#223)" (#275)
This reverts commit 6768658300.
See details in #PR266
2018-01-01 19:32:58 +01:00
Janne Sinivirta
59546b623e Merge pull request #269 from gcarq/pyup-update-pandas-0.21.1-to-0.22.0
Update pandas to 0.22.0
2018-01-01 07:47:59 +02:00
Gérald LONLAS
0a5463fee8 Merge pull request #267 from gcarq/update_config_example
Add pair_blacklist sample in config.json.example
2017-12-31 11:19:51 -08:00
pyup-bot
cdfb18e9b4 Update pandas from 0.21.1 to 0.22.0 2017-12-31 14:21:50 +01:00
Gerald Lonlas
1f635d3793 Add pair_blacklist in config.example 2017-12-31 01:14:17 -08:00
Gerald Lonlas
714d77dbd8 Add expiremental feature to sell only if we make a profit 2017-12-30 18:14:10 -08:00
Gérald LONLAS
9803130848 Merge pull request #259 from gcarq/fix/issue-248
Fix issue #248: missing configuration when executing /forcesell
2017-12-30 17:28:16 -08:00
Samuel Husso
ad44d8d42a Merge pull request #263 from jblestang/fix_issue_262
Fixing bug #262
2017-12-30 17:01:00 +02:00
Jean-Baptiste LE STANG
68f81b2abb autopep8 is going to be my new friend 2017-12-30 15:55:49 +01:00
Jean-Baptiste LE STANG
4945331093 Fixing the positional parameter naming + unit tests updated 2017-12-30 15:43:22 +01:00
jblestang
8411844d7e Implement pair_blacklist functionality (#257)
* Adding an optional black_list of pairs not to be traded

* applying the blacklist also when not using --dynamic-whitelist

* fix error retrieving pair in conf

* Refactoring the handling of whitelist among the various functions

* unit test to verify that black listed pairs are being removed from the pair_whitelist

* Fixing newly added unit tests in develop

* fixing flake8 code review

* fix code review from @garcq
2017-12-30 14:15:07 +01:00
Janne Sinivirta
00415d66a2 Merge pull request #260 from gcarq/increase_code_coverage
Increase code coverage
2017-12-30 14:02:33 +02:00
kryofly
f7398e615a Improve backtesting tests (#256)
* test bugfix dataframe trimming

* flake8 (as usual)

* tests backtesting cleanup and bugfix

* flake8

* test backtesting::start()

* tests cleanup set() usage

* tests: add missing assert
2017-12-30 11:55:23 +01:00
Gerald Lonlas
e81a9cbb17 Increase code coverage
Change log:
* Increase code coverage for test_exchange.py
* Move Exchange Unit tests files tests/exchange/
* Move RPC Unit tests files tests/rpc/
2017-12-29 23:37:02 -08:00
Gerald Lonlas
c8c8c626b0 Fix issue #248: missing configuration when executing /forcesell
This is not a beautiful workaround, I am not proud of it,
but a redesigning of main.py and telegram.py will be
necessary for a better integration. Any better solution
is welcome.
2017-12-29 20:03:12 -08:00
Janne Sinivirta
9f5f0ddaaa Merge pull request #243 from gcarq/pyup-update-pymarketcap-3.3.139-to-3.3.141
Update pymarketcap to 3.3.141
2017-12-29 19:31:50 +02:00
Janne Sinivirta
80e1e64eae Merge pull request #249 from kryofly/tests_dec28
tests for dataframe, whitelist and backtesting
2017-12-29 19:14:57 +02:00
kryofly
37613fc056 flake8 2017-12-29 17:53:58 +01:00
Janne Sinivirta
57c6aefe38 Merge branch 'develop' into tests_dec28 2017-12-29 16:34:00 +02:00
Janne Sinivirta
133c467cf4 Merge branch 'develop' into tests_dec28 2017-12-29 16:33:12 +02:00
Janne Sinivirta
900cab4b42 Merge pull request #253 from kryofly/sell_signal
execute sell if get_signal OR ROI reached
2017-12-29 16:31:37 +02:00
Janne Sinivirta
f9cc556971 Merge branch 'develop' into sell_signal 2017-12-29 16:27:04 +02:00
Janne Sinivirta
f2ce367cec Merge branch 'develop' into sell_signal 2017-12-29 16:26:23 +02:00
Janne Sinivirta
fba9cbcff6 Merge pull request #247 from gcarq/add_unittest
Refactor Optimize tests, and add more unit tests
2017-12-29 16:23:36 +02:00
kryofly
3e0458da7d flake8 2017-12-29 09:40:24 +01:00
Gerald Lonlas
0d605d2396 Refactor Optimize tests, and add more unit tests 2017-12-28 22:32:48 -08:00
Janne Sinivirta
145583f0b7 Merge pull request #244 from jblestang/fix_daily_profit
Fixing daily profit,
2017-12-29 06:05:25 +02:00
kryofly
847dde0d65 execute sell if get_signal OR ROI reached 2017-12-29 00:07:54 +01:00
kryofly
ab112581a7 tests: anal stretching to accomodate flake8 2017-12-28 20:05:33 +01:00
kryofly
f48f5d0f31 tests for dataframe, whitelist and backtesting 2017-12-28 15:58:19 +01:00
Janne Sinivirta
0abf0b0e39 Merge pull request #242 from gcarq/backtesting-unittests
Backtesting and hyperopt unit tests
2017-12-28 12:45:28 +02:00
pyup.io bot
965616b214 Update sqlalchemy from 1.1.15 to 1.2.0 (#245) 2017-12-28 10:11:32 +01:00
Janne Sinivirta
a36fd00f6a also print dot when hyperopt eval result is fail 2017-12-28 06:40:11 +02:00
Janne Sinivirta
7f44ba6df4 unit tests for optimize.hyperopt 2017-12-28 06:39:56 +02:00
Janne Sinivirta
7b0beb0afa cleanups 2017-12-28 06:36:18 +02:00
Janne Sinivirta
ae0a1436e2 match test files to prod files for backtesting/hyperopt 2017-12-28 06:35:09 +02:00
Jean-Baptiste LE STANG
8537e9f40f CI flake8 error 2017-12-27 21:33:42 +01:00
Jean Baptiste LE STANG
d61d88559c Fixing daily profit, taking into account the time part of the date (removing it in fact) 2017-12-27 21:06:05 +01:00
Janne Sinivirta
9b4c0f01f2 more unit tests for backtesting 2017-12-27 17:39:54 +02:00
Gérald LONLAS
6c8253a4f5 Add more unittest (#241) 2017-12-27 11:41:11 +01:00
pyup-bot
6464373636 Update pymarketcap from 3.3.139 to 3.3.141 2017-12-27 10:19:45 +01:00
Janne Sinivirta
dcd0a0ec61 Merge pull request #239 from glonlas/feature/value_in_fiat
Display profits in fiat
2017-12-27 11:19:38 +02:00
Gerald Lonlas
ff6b0fc1c9 Display profits in fiat 2017-12-26 19:44:19 -08:00
Michael Egger
a514b92dcf catch MIN_TRADE_REQUIREMENT_NOT_MET as non-critical exception (#237)
* add MIN_TRADE_REQUIREMENT_NOT_MET to response validation

* implement test
2017-12-26 09:39:29 +01:00
Janne Sinivirta
de33d69eed Lint fixes (#236)
* correct docstring

* add type annotation to trade_count_lock

* fix indentations

* allow globals in hyperopt.py

* fix import order

* simplify asserts

* use proper variable name

* simplify condition

* fix path operation that fails on windows
2017-12-25 12:07:50 +01:00
Janne Sinivirta
9959d53f5e Logging improvements to Hyperopt (#235)
* make log texts go on new line

* remove unnecessary fields from hyperopt log messages

* shorten log text in hyperopt

* consider making zero trades a failed hyperopt eval

* only log from hyperopt when result improves

* remove unnecessary temp variables

* remove unused result data variables

* remove unused import

* fix an outdated comment
2017-12-25 08:18:34 +01:00
Pan Long
6768658300 Make get_signals async. This should speed up create_trade calls by at least 10x. (#223) 2017-12-25 07:01:01 +01:00
Samuel Husso
433bf409f4 Merge pull request #232 from gcarq/tweak-hyperopt
Tweak Hyperopt
2017-12-23 19:25:45 +02:00
Janne Sinivirta
353b0d2d34 balance hyperopt objective to adjusted profit calculations 2017-12-23 19:18:28 +02:00
Janne Sinivirta
e644d57dbe log should state profit is in BTC to avoid confusion 2017-12-23 19:00:49 +02:00
Janne Sinivirta
50e7cef5f3 remove commented-out code 2017-12-23 19:00:49 +02:00
Janne Sinivirta
1058820e1b just pass stake_amount instead of the whole config 2017-12-23 19:00:49 +02:00
Janne Sinivirta
24bc3a8390 show more digits for profits 2017-12-23 15:11:19 +02:00
Janne Sinivirta
5309ea3820 use newline for each log result for readability 2017-12-23 15:11:19 +02:00
Janne Sinivirta
a063680d32 calculate log line only if really logging 2017-12-23 15:11:19 +02:00
Janne Sinivirta
10cf2ce853 remove unnecessary confusing division 2017-12-23 15:11:19 +02:00
Janne Sinivirta
871357a2e3 just require positive results 2017-12-23 15:11:19 +02:00
Janne Sinivirta
efe0d77dbb Merge pull request #231 from gcarq/fix/hyperopt-filter-nan
filter nan values from total_profit and avg_profit
2017-12-23 15:07:40 +02:00
Samuel Husso
8d93363655 filter nan values from total_profit and avg_profit 2017-12-23 09:21:04 +02:00
Samuel Husso
b6dd9dd227 Merge pull request #227 from gcarq/create-contribute-guideline
Create contribution guideline
2017-12-22 19:00:49 +02:00
Janne Sinivirta
95c6ada2ad link to contribution guide from README.md 2017-12-22 14:31:08 +02:00
Janne Sinivirta
11585f9581 Create contribution guideline 2017-12-22 14:29:31 +02:00
Janne Sinivirta
8085a7b237 Merge pull request #215 from seansan/patch-1
add % in status table for profit
2017-12-22 14:09:06 +02:00
Janne Sinivirta
c99e2c12ba Merge branch 'develop' into patch-1 2017-12-22 14:05:09 +02:00
Janne Sinivirta
44a4ff0cb2 Merge branch 'develop' into patch-1 2017-12-22 13:58:13 +02:00
Janne Sinivirta
f300af0fe2 Merge pull request #200 from glonlas/fix_fees_calculation
Fix the fee calculation
2017-12-22 13:55:02 +02:00
Samuel Husso
ff186c7f65 Merge pull request #218 from glonlas/fix_hyperopt
Fix hyperopt when using MongoDB
2017-12-22 10:48:45 +02:00
Gerald Lonlas
41e22657e4 Fix hyperopt when using MongoDB 2017-12-21 19:20:47 -08:00
Samuel Husso
974815cb14 Merge pull request #220 from seansan/patch-2
added Minimal (advised) system requirements
2017-12-21 10:16:47 +02:00
seansan
33beab9c47 added Minimal (advised) system requirements 2017-12-21 09:13:26 +01:00
Gerald Lonlas
d258118b0a Fix the fee calculation, backtesting, and hyperopt fee calculation and avg_profit 2017-12-20 20:18:41 -08:00
seansan
4dab39ed9e add % in status table for profit 2017-12-20 13:58:18 +01:00
Janne Sinivirta
33293d5cdd Merge pull request #205 from gcarq/fix/travis-curl-redirect
pass follow redirects for curl to fix travis
2017-12-19 09:26:42 +02:00
Samuel Husso
285308dcbe pass follow redirects for curl to fix travis 2017-12-19 08:27:52 +02:00
Janne Sinivirta
c8fb6c4661 More lint fixes (#198)
* autopep fixes

* remove unused imports

* fix plot_dataframe.py lint warnings

* make pep8 error fails the build

* two more line breakings

* matplotlib.use() must be called before pyplot import
2017-12-18 17:36:00 +01:00
Janne Sinivirta
1a556198b2 Merge pull request #203 from gcarq/travis/fix-ssl
use curl instead of wget (see travis-ci/issues/5059)
2017-12-18 11:09:50 +02:00
Samuel Husso
98650acca0 use curl instead of wget (see travis-ci/issues/5059) 2017-12-18 10:26:48 +02:00
Samuel Husso
123f2781a1 Merge pull request #202 from gcarq/cache-talib
Cache TAlib
2017-12-18 10:06:24 +02:00
Janne Sinivirta
92f6db5bd7 fix checking for cached ta-lib 2017-12-18 09:36:29 +02:00
Janne Sinivirta
e5f8c1e75d cache ta-lib folder, skip build if cache exists 2017-12-18 09:29:17 +02:00
Janne Sinivirta
4c0a316e3e enable sudo for installing talib 2017-12-18 09:20:52 +02:00
Gerald Lonlas
d613d63fdc Fix the fee calculation 2017-12-17 23:01:34 -08:00
Janne Sinivirta
e3941cde7e move wgetting and building of talib to an sh file 2017-12-18 07:15:14 +02:00
Janne Sinivirta
642422d5c4 cache pip dependencies (#199) 2017-12-17 21:19:50 +01:00
Samuel Husso
117ec4e64d Merge pull request #195 from gcarq/feature/travis-smoke-tests
add smoke tests to run a round of hyperopt and backtesting
2017-12-17 15:45:14 +02:00
Samuel Husso
0219584bfe Merge pull request #197 from gcarq/fix_plotting
Fix plotting broken by refactoring
2017-12-17 15:43:01 +02:00
Janne Sinivirta
d3947fc893 create config.json for backtesting 2017-12-17 15:19:35 +02:00
Janne Sinivirta
fe0c26f536 create config.json for hyperopt 2017-12-17 15:13:39 +02:00
Janne Sinivirta
e83e4909a0 install freqtrade module for hyperopting 2017-12-17 15:01:11 +02:00
Janne Sinivirta
ed05a1db9d Merge branch 'develop' into feature/travis-smoke-tests 2017-12-17 14:51:26 +02:00
Janne Sinivirta
21a11f5589 run pytest, hyperopt and backtesting in parallel 2017-12-17 14:45:31 +02:00
Janne Sinivirta
6288adfefd fix plotting broken by refactoring 2017-12-17 14:14:57 +02:00
Janne Sinivirta
6a1caafb7a Merge pull request #196 from gcarq/fix/hyperopt
fix hyperopt not getting default ticker_interval
2017-12-17 13:50:25 +02:00
Samuel Husso
ce51749177 fix hyperopt not getting default ticker_interval 2017-12-17 12:34:26 +02:00
Samuel Husso
a68ca31684 add smoke test commands under script block 2017-12-17 12:01:08 +02:00
Samuel Husso
5f1b9943d1 add smoke tests to run a round of hyperopt and backtesting 2017-12-17 11:55:34 +02:00
Janne Sinivirta
155ed4e501 Merge pull request #191 from gcarq/feature/add-systemd-service-file
add systemd service file
2017-12-17 07:43:20 +02:00
Janne Sinivirta
80ef2cfed4 Merge pull request #193 from gcarq/feature/ci-enforce-pep8
CI: enforce PEP8 conform code
2017-12-17 07:42:23 +02:00
Janne Sinivirta
5efc417690 Merge pull request #192 from gcarq/feature/forcesell-handle-open-orders
/forcesell: handle trades with open orders
2017-12-17 07:41:51 +02:00
Gérald LONLAS
14868615d5 Add mock to improve backtesting tests (#194) 2017-12-17 00:24:21 +01:00
Gérald LONLAS
512fcdbcb1 Allow user to update testdata files with parameter --refresh-pairs-cached (#174) 2017-12-16 15:42:28 +01:00
gcarq
6f2caf9698 invoke flake8 after success 2017-12-16 03:44:49 +01:00
gcarq
a395a14eeb adapt README 2017-12-16 03:40:06 +01:00
gcarq
95fe0f4dec fix pep8 warnings 2017-12-16 03:39:47 +01:00
gcarq
f6d85e021f add setup.cfg to configure flake8 2017-12-16 03:28:59 +01:00
gcarq
597f08e2a2 update README 2017-12-16 03:00:51 +01:00
gcarq
df4784e7b9 add service file 2017-12-16 03:00:43 +01:00
gcarq
ddd3d2d0a9 ignore cancelled order during trade state update 2017-12-16 02:36:43 +01:00
gcarq
cb4ecfd3a3 move function 2017-12-16 01:37:06 +01:00
gcarq
f4b59492ab fix NoneType issue 2017-12-16 01:31:15 +01:00
gcarq
ae37f49b51 /forcesell: handle trades with open orders 2017-12-16 01:09:07 +01:00
gcarq
6e68315d2c reorder imports 2017-12-15 23:58:21 +01:00
gcarq
c1c9dd03ce /daily: fix identation and simplify loops 2017-12-15 23:56:02 +01:00
Gérald LONLAS
e00f02b603 Improve telegram /profit command (#188) 2017-12-15 17:19:00 +01:00
pyup.io bot
9f907d5b5e Update python-bittrex from 0.2.1 to 0.2.2 (#189) 2017-12-15 16:10:10 +01:00
Samuel Husso
6729574201 Merge pull request #186 from glonlas/update_daily_command
Improve  /daily command
2017-12-15 08:19:02 +02:00
Gerald Lonlas
2a2af4878e Update /daily command, reorder telegram menu, limit /daily profit at 8 decimals 2017-12-14 21:18:52 -08:00
Michael Egger
bfb3e09d1d raise ContentDecodingError if bittrex responds with NO_API_RESPONSE (#183) 2017-12-14 20:27:04 +01:00
Pan Long
89ee0654f4 Use ENTRYPOINT instead of CMD so additional arguments can be supplied for docker run. (#184) 2017-12-14 18:41:40 +01:00
Gérald LONLAS
2ac8b685d6 Add param for Dry run to use a DB file instead of memory (#182) 2017-12-14 15:10:11 +01:00
Samuel Husso
4b38100ae2 Merge pull request #175 from gcarq/pyup-update-pandas-0.21.0-to-0.21.1
Update pandas to 0.21.1
2017-12-13 08:18:31 +02:00
pyup-bot
d6c14d5258 Update pandas from 0.21.0 to 0.21.1 2017-12-13 06:18:06 +01:00
Samuel Husso
cb09cabbdd Merge pull request #171 from stephendade/dailymsg
Added daily profit telegram command
2017-12-12 19:42:31 +02:00
Janne Sinivirta
77023c0ecf Merge pull request #169 from jblestang/fix_ticker_interval
Fix ticker interval
2017-12-12 17:21:55 +02:00
Stephen Dade
0b18c93d19 Daily profit command - better message formatting and minor fixes 2017-12-12 19:41:25 +11:00
Jean-Baptiste LE STANG
0617753a7f Adding a test unit for 1 minute ticker interval 2017-12-11 22:11:06 +01:00
Janne Sinivirta
b77fad6e5f Merge pull request #173 from glonlas/autoselect_top_currencies
Allow to change the number of currencies used by dynamic-whitelist
2017-12-11 18:04:10 +02:00
Gerald Lonlas
90bf6f2d4a Remove unecessary import 2017-12-11 00:07:36 -08:00
Gerald Lonlas
ef7646417b Allow to change the number of currencies used by dynamic-whitelist 2017-12-11 00:01:27 -08:00
Samuel Husso
01874e379f Merge pull request #172 from gcarq/new_pair_set
New currency pair set
2017-12-11 09:33:05 +02:00
Janne Sinivirta
7afd8da28f fix a broken unit test due to changing test dataset 2017-12-10 13:56:39 +02:00
Janne Sinivirta
3d532c6015 update backtest data to match pairs in config.json.example 2017-12-10 11:17:01 +02:00
Janne Sinivirta
a692ef6715 update example coins to be from monthly max volume list 2017-12-10 11:16:28 +02:00
Stephen Dade
ccb8c3c352 Added daily profit telegram command 2017-12-10 17:32:40 +11:00
toto
18f01113c2 use the CLI arguments as the ticker interval 2017-12-09 11:51:53 +01:00
toto
f7def09dec fix for the ticker interval set by default to 5 2017-12-09 11:39:26 +01:00
Janne Sinivirta
82bf0be3e2 Merge pull request #168 from gcarq/pyup-update-python-telegram-bot-8.1.1-to-9.0.0
Update python-telegram-bot to 9.0.0
2017-12-09 07:33:36 +02:00
pyup-bot
212f4fdd95 Update python-telegram-bot from 8.1.1 to 9.0.0 2017-12-08 23:21:03 +01:00
Samuel Husso
a5058ff999 Merge pull request #164 from gcarq/pyup-update-pytest-3.3.0-to-3.3.1
Update pytest to 3.3.1
2017-12-06 09:07:18 +02:00
pyup-bot
ea1c16f2ac Update pytest from 3.3.0 to 3.3.1 2017-12-06 05:15:53 +01:00
Janne Sinivirta
67337fadaa Merge pull request #157 from gcarq/pyup-update-pytest-3.2.5-to-3.3.0
Update pytest to 3.3.0
2017-12-03 10:02:03 +02:00
Janne Sinivirta
94c1d66e59 Merge pull request #159 from gcarq/pyup-update-tabulate-0.8.1-to-0.8.2
Update tabulate to 0.8.2
2017-12-03 10:01:29 +02:00
Janne Sinivirta
510e6edfbf Merge pull request #156 from gcarq/pyup-update-arrow-0.10.0-to-0.12.0
Update arrow to 0.12.0
2017-12-03 09:40:02 +02:00
Janne Sinivirta
e8c31142ae Merge pull request #154 from gcarq/hyperopt/simplify-logging
Hyperopt/simplify logging
2017-12-03 09:39:45 +02:00
pyup-bot
71c780a530 Update tabulate from 0.8.1 to 0.8.2 2017-12-03 08:34:08 +01:00
pyup-bot
7e579de163 Update pytest from 3.2.5 to 3.3.0 2017-12-03 08:34:01 +01:00
pyup-bot
dd1a52c534 Update arrow from 0.10.0 to 0.12.0 2017-12-03 08:33:57 +01:00
Janne Sinivirta
e815a43164 Merge pull request #137 from gcarq/pyup-initial-update
Initial Update
2017-12-03 09:33:50 +02:00
Janne Sinivirta
2f17706e76 Merge pull request #155 from gcarq/maintenance/remove-btc-time
remove BTC_TIME
2017-12-02 15:29:10 +02:00
Samuel Husso
86a94798dd BTC_TIME will be removed from bittrex Dec 8th 2017-12-02 15:06:33 +02:00
Samuel Husso
a7cca4985e omit hyperopt output if total_profit doesn't go pass threashold (3) 2017-12-02 01:32:23 +02:00
Samuel Husso
965c075362 disable info logging on hyperopt.tpe 2017-12-02 00:21:46 +02:00
Janne Sinivirta
05d7746f62 Revert "Update networkx from 1.11 to 2.0"
This reverts commit 0502bd3c2d.
2017-12-01 21:13:02 +02:00
Samuel Husso
688326b58c Merge pull request #146 from gcarq/feature/integrate-backtesting
integrate backtesting/hyperopt into freqtrade.optimize
2017-11-30 08:19:59 +02:00
gcarq
0c9993cc89 convert bash scripts to python scripts 2017-11-25 15:40:19 +01:00
gcarq
0c35e6ad19 minor changes 2017-11-25 03:28:52 +01:00
gcarq
68521ea46c adapt README 2017-11-25 03:28:39 +01:00
gcarq
2fe11cd77a add helper scripts for mongodb 2017-11-25 03:28:18 +01:00
gcarq
e27a6a7a91 add mongodb support for hyperopt parallelization 2017-11-25 02:04:37 +01:00
gcarq
5bf583cba4 remove unused imports 2017-11-25 01:23:18 +01:00
gcarq
a23fce519d pretty print hyperopt results 2017-11-25 01:22:36 +01:00
gcarq
7f3f127165 remove custom env from .travis.yml 2017-11-25 01:13:28 +01:00
gcarq
9ff1f05e66 add --epochs to hyperopt subcommand 2017-11-25 01:12:44 +01:00
gcarq
b9c4eafd96 integrate hyperopt and implement subcommand 2017-11-25 01:04:11 +01:00
gcarq
7fa5846c6b move hyperopt to freqtrade.optimize.hyperopt 2017-11-25 00:30:39 +01:00
gcarq
3b37f77a4d move backtesting to freqtrade.optimize.backtesting 2017-11-24 23:58:35 +01:00
Michael Egger
858d2329e5 add experimental flag support and add use_sell_signal (#143)
* add use_sell_signal to config schema

* check use_sell_signal

* set use_sell_signal to false
2017-11-24 21:58:00 +01:00
Mathieu Favréaux
371ee1e457 In backtesting, ensure we don't buy the same pair again before selling (#139)
* in backtesting, ensure we don't buy before we sell

* no overlapping trades only if max_open_trades > 0

* --limit-max-trades now --realistic-simulation
2017-11-24 21:09:44 +01:00
Geka000
cfbfe90aa0 keyboard markup for telegram bot (#142) 2017-11-24 20:54:50 +01:00
Michael Egger
fd30f5dc59 Merge branch 'develop' into pyup-initial-update 2017-11-23 21:49:56 +01:00
pyup-bot
0502bd3c2d Update networkx from 1.11 to 2.0 2017-11-23 21:07:43 +01:00
pyup-bot
3ce7ef5e8b Update pytest from 3.2.3 to 3.2.5 2017-11-23 21:07:42 +01:00
pyup-bot
2324aa0782 Update scipy from 0.19.1 to 1.0.0 2017-11-23 21:07:40 +01:00
pyup-bot
6a57a8da12 Update scikit-learn from 0.19.0 to 0.19.1 2017-11-23 21:07:39 +01:00
pyup-bot
9276f3202c Update pandas from 0.20.3 to 0.21.0 2017-11-23 21:07:37 +01:00
pyup-bot
a6598997e2 Update sqlalchemy from 1.1.14 to 1.1.15 2017-11-23 21:07:36 +01:00
gcarq
82913cd3f4 upgrade python-bittrex to 0.2.1 2017-11-23 20:53:13 +01:00
gcarq
be6939ee8a use 8 digits of precision for amount and rate in formatting 2017-11-23 20:52:07 +01:00
Samuel Husso
7ba4a5d24b Merge pull request #136 from gcarq/stoploss_tweak
Stoploss tweak
2017-11-23 19:54:08 +02:00
Janne Sinivirta
371e6d99c9 set stoploss to -10% 2017-11-23 18:43:19 +02:00
Janne Sinivirta
84b105c82b fix invalid json in example config 2017-11-23 18:41:25 +02:00
Janne Sinivirta
c6def418cf Merge pull request #135 from rybolov/develop
Better buy and sell strategy
2017-11-23 18:25:56 +02:00
Michael Smith
5fce2c5712 Better buy and sell strategy:
Buy if at the low end of normal range and the price is increasing.
Buy into extreme gains regardless of if it's on the low part of the range.
Avoid buying when the price is on a long decrease even if it's low.
Sell anytime the price is above the top end of normal range and the momentum slows.
Sell on an extreme drop.
2017-11-23 22:33:41 +08:00
Janne Sinivirta
aacd7d8987 Merge pull request #131 from gcarq/feature/backtesting-max-open-trades
implement trade count lock for backtesting
2017-11-23 16:16:43 +02:00
gcarq
4a707d7452 add --limit-max-trades 2017-11-23 00:25:06 +01:00
Janne Sinivirta
21551b3c40 Merge pull request #133 from gcarq/feature/fix-buy-amount-calc
fix LIMIT_BUY amount calculation
2017-11-22 22:31:25 +02:00
gcarq
7727f2cc8f implement test 2017-11-22 21:02:36 +01:00
gcarq
9a87dcf0a1 dont apply fees on trade creation 2017-11-22 21:01:44 +01:00
gcarq
9136e64d89 force flush in create_trade and execute_sell (fixes #128) 2017-11-22 20:51:25 +01:00
Samuel Husso
765a762ccf Merge pull request #122 from gcarq/feature/fix-signal-handling
fix signal handling
2017-11-22 13:38:57 +02:00
gcarq
02ca2ed585 implement trade count lock for backtesting 2017-11-21 22:33:34 +01:00
gcarq
f3ba3ddd54 move buy_price and sell_price to plotting script 2017-11-21 20:41:49 +01:00
gcarq
65ce948b0b catch ValueErrors from analyze_ticker (fixes #123) 2017-11-21 20:37:29 +01:00
gcarq
383a9f6eeb catch BaseException to force stdout flush when process dies 2017-11-21 20:24:52 +01:00
Janne Sinivirta
43dda9c9cf Merge pull request #125 from gcarq/conf-update
update conf example
2017-11-21 09:38:25 +02:00
Samuel Husso
7a44a1d1c1 match example config to backtest_conf and update README to fix #124 2017-11-21 07:37:31 +02:00
gcarq
5d934cd5b6 enhance open order formatting in status handle 2017-11-20 23:33:52 +01:00
gcarq
788cda4925 add missing import 2017-11-20 22:26:32 +01:00
gcarq
55a69e4a45 use normal program flow to handle interrupts 2017-11-20 22:15:19 +01:00
gcarq
1931d31147 Merge tag '0.14.3' into develop
0.14.3
2017-11-20 20:01:23 +01:00
gcarq
e9dbdc9247 Merge branch 'release/0.14.3' 2017-11-20 20:01:18 +01:00
gcarq
86b6c6f334 version bump 2017-11-20 20:01:10 +01:00
gcarq
cd5afd6ff4 use jsonschema regex pattern for whitelist format and enhance validation error messages (closes #120) 2017-11-20 19:37:25 +01:00
Janne Sinivirta
d88cc084e6 align numbers in hyperopt print out (#119) 2017-11-20 10:22:11 +01:00
Jeff Pipas
5deaebf0c2 Tests now use UTC time with arrow instead of datetime (#117)
* fixing tests to use arrow-utc

* removing datetime import
2017-11-19 04:58:35 +01:00
gcarq
19734ad863 set bootstrap_retries to infinite (fixes #113) 2017-11-18 22:23:05 +01:00
gcarq
b16ccb9919 handle requests exception in validate_pairs 2017-11-18 22:22:45 +01:00
gcarq
d41837817c move logging to freqtrade.rpc 2017-11-18 21:43:21 +01:00
gcarq
3ab14dfe39 add middleware to expose common functionality for multiple rpc implementations 2017-11-18 21:30:31 +01:00
Michael Egger
4a91ecd91a Merge pull request #115 from gcarq/pylint_cleanups
Pylint cleanups
2017-11-18 16:00:21 +01:00
Samuel Husso
a3da2911e8 Merge pull request #114 from gcarq/new_algo
New buy strategy
2017-11-18 13:09:40 +02:00
Janne Sinivirta
6f5b418f0b small balancing to hyperopt objective 2017-11-18 10:24:18 +02:00
Janne Sinivirta
57691c82b1 whitelist TA-lib in pylint 2017-11-18 10:13:14 +02:00
Janne Sinivirta
37a74b38ba more little pylint fixes 2017-11-18 10:09:19 +02:00
Janne Sinivirta
9ab81a987d fix pylint warnings in test_main.py 2017-11-18 09:58:55 +02:00
Janne Sinivirta
4b08e3d571 fix pylint warnings in __init__ files 2017-11-18 09:58:29 +02:00
Janne Sinivirta
187fea0c28 disable bunch of meaningless pylint warnings 2017-11-18 09:45:01 +02:00
Janne Sinivirta
4e54b27398 use parentheses for multiline string instead of backslash 2017-11-18 09:44:28 +02:00
Janne Sinivirta
aced5cc3ba rename variable to remove Mypy warning of type error 2017-11-18 09:43:42 +02:00
Janne Sinivirta
669ec30413 remove unused import 2017-11-18 09:34:57 +02:00
Janne Sinivirta
0082b7abdd add missing module and class docstring 2017-11-18 09:34:32 +02:00
Janne Sinivirta
7903f3a546 fix test name 2017-11-18 09:19:22 +02:00
Janne Sinivirta
ec75586bdd new buy strategy 2017-11-18 08:45:57 +02:00
Janne Sinivirta
df9902d6a4 Merge pull request #107 from gcarq/feature/add-backtesting-subcommand
add backtesting subcommand and refresh test data
2017-11-18 08:13:42 +02:00
Janne Sinivirta
315919cdd6 fix platform dependent bug in argparse test 2017-11-18 08:07:37 +02:00
gcarq
63c95a3546 modify trade life cycle (should fix #112) 2017-11-17 20:17:29 +01:00
gcarq
59d04d1d0c catch TelegramError (fixes #113) 2017-11-17 19:49:03 +01:00
gcarq
d1cc9e868b adapt README 2017-11-17 19:03:08 +01:00
gcarq
14de46576b use load_backtesting_data 2017-11-17 18:23:40 +01:00
gcarq
bdff29a472 remove code duplicates 2017-11-17 18:17:59 +01:00
gcarq
8655c6c264 reduce backtest data samples to 10 2017-11-17 18:15:25 +01:00
gcarq
3f4e4a23a0 add argparse handling tests 2017-11-17 18:15:24 +01:00
gcarq
b682262486 refactor argparse handling 2017-11-17 18:15:24 +01:00
gcarq
5be7be6189 adapt tests 2017-11-17 18:15:24 +01:00
gcarq
3475a07522 fetching new testing data for oneMin and fiveMin intervals 2017-11-17 18:15:24 +01:00
gcarq
fb7ea169d4 fix some formatting issues 2017-11-17 18:13:34 +01:00
gcarq
5469293e5f use tabulate to format backtesting result 2017-11-17 18:13:02 +01:00
gcarq
9b644b0305 add --ticker-interval 2017-11-17 18:09:55 +01:00
gcarq
0df1404d6a fix typo 2017-11-17 18:09:55 +01:00
gcarq
bb4a9ed20f implement backtest subcommand 2017-11-17 18:09:55 +01:00
Samuel Husso
77887d6fbc Merge pull request #111 from gcarq/memoryfix-hyperopt
Memory fix hyperopt
2017-11-17 18:41:38 +02:00
Janne Sinivirta
d89db50465 avoid copy operation due to memory consumption 2017-11-17 12:30:54 +02:00
Janne Sinivirta
632d00e01d move price point calculations out from populate functions 2017-11-17 12:30:03 +02:00
Janne Sinivirta
2a56031cdc remove unnecessary line 2017-11-17 12:30:03 +02:00
Janne Sinivirta
16d412323c add a little snippet to allow running line_profiler with hyperopt 2017-11-16 20:43:24 +02:00
Janne Sinivirta
27a6b29c80 move time diff calculation out of a loop 2017-11-16 20:43:24 +02:00
Janne Sinivirta
5d1f874041 switch ix to loc, ix is apparently deprecated 2017-11-16 20:43:24 +02:00
Janne Sinivirta
174122a09b remove unnecessary calculation 2017-11-16 20:38:59 +02:00
Janne Sinivirta
1b6a60ecb2 refactor backtesting to avoid recalculating indicators in hyperopt 2017-11-16 20:38:46 +02:00
Michael Egger
1ccb266032 Merge pull request #104 from gcarq/sell_signal
Add sell_signal support
2017-11-16 17:02:24 +01:00
Janne Sinivirta
a963f1820c rename should_sell to min_roi_reached 2017-11-16 16:53:34 +01:00
Janne Sinivirta
b9983149ef plug sell strategy to backtesting 2017-11-16 16:53:34 +01:00
Janne Sinivirta
c1ef3f526c remove unnecessary comparison 2017-11-16 16:53:34 +01:00
Janne Sinivirta
6b7afb80b2 fix failing test 2017-11-16 16:53:34 +01:00
Janne Sinivirta
0b8afa12e9 exit strategy after roi check 2017-11-16 16:53:34 +01:00
Janne Sinivirta
1db0a7d4ce populate sell signal 2017-11-16 16:53:34 +01:00
Janne Sinivirta
c12a9ebd92 make signal getting parametrized 2017-11-16 16:53:34 +01:00
gcarq
d86dcc4752 check if result exists in get_ticker (fixes #106) 2017-11-16 16:39:06 +01:00
gcarq
0bc96241d5 rework exception handling (fixes #108) 2017-11-16 16:14:43 +01:00
gcarq
a0bb7a61e6 Merge tag '0.14.2' into develop
0.14.2
2017-11-16 00:40:48 +01:00
97 changed files with 6522 additions and 954 deletions

30
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,30 @@
## 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 it hasn't been reported, please create a new issue.
## Step 2: Describe your environment
* Python Version: _____ (`python -V`)
* 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:
1. _____
2. _____
3. _____
### Observed Results:
* What happened?
* What did you expect to happen?
### Relevant code exceptions or logs:
```
// paste your log here
```

15
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,15 @@
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)
## Summary
Explain in one sentence the goal of this PR
Solve the issue: #___
## Quick changelog
- <change log #1>
- <change log #2>
## What's new?
*Explain in details what this PR solve or improve. You can include visuals.*

14
.gitignore vendored
View File

@@ -1,3 +1,11 @@
# Freqtrade rules
freqtrade/tests/testdata/*.json
hyperopt_conf.py
config.json
*.sqlite
.hyperopt
logfile.txt
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -73,11 +81,9 @@ target/
# pyenv
.python-version
config.json
preprocessor.py
*.sqlite
.env
.venv
.idea
.vscode
hyperopt_trials.pickle

View File

@@ -1,3 +1,10 @@
[MASTER]
extension-pkg-whitelist=numpy,talib,talib.abstract
[BASIC]
good-names=logger
ignore=vendor
[TYPECHECK]
ignored-modules=numpy,talib,talib.abstract

View File

@@ -1,12 +1,9 @@
sudo: false
sudo: true
os:
- linux
language: python
python:
- 3.6
env:
- BACKTEST=
- BACKTEST=true
addons:
apt:
packages:
@@ -14,16 +11,27 @@ addons:
- libdw-dev
- binutils-dev
install:
- wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
- tar zxvf ta-lib-0.4.0-src.tar.gz
- cd ta-lib && ./configure && sudo make && sudo make install && cd ..
- ./install_ta-lib.sh
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install coveralls
- pip install --upgrade flake8 coveralls
- pip install -r requirements.txt
script:
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
- pip install -e .
jobs:
include:
- script: pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
- script:
- cp config.json.example config.json
- python freqtrade/main.py backtesting
- script:
- cp config.json.example config.json
- python freqtrade/main.py hyperopt -e 5
- script: flake8 freqtrade
after_success:
- coveralls
notifications:
slack:
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
cache:
directories:
- $HOME/.cache/pip
- ta-lib

45
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,45 @@
# Contribute to freqtrade
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
- Create your PR against the `develop` branch, not `master`.
- New features need to contain unit tests and must be PEP8
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.
**Before sending the PR:**
## 1. Run unit tests
All unit tests must pass. If a unit test is broken, change your code to
make it pass. It means you have introduced a regression.
**Test the whole project**
```bash
pytest freqtrade
```
**Test only one file**
```bash
pytest freqtrade/tests/test_<file_name>.py
```
**Test only one method from one file**
```bash
pytest freqtrade/tests/test_<file_name>.py::test_<method_name>
```
## 2. Test if your code is PEP8 compliant
**Install packages** (If not already installed)
```bash
pip3.6 install flake8 coveralls
```
**Run Flake8**
```bash
flake8 freqtrade
```

View File

@@ -20,4 +20,4 @@ RUN pip install -r requirements.txt
# Install and execute
COPY . /freqtrade/
RUN pip install -e .
CMD ["freqtrade"]
ENTRYPOINT ["freqtrade"]

295
README.md
View File

@@ -1,157 +1,198 @@
# 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)](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
[![Coverage Status](https://coveralls.io/repos/github/gcarq/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
Simple High frequency trading bot for crypto currencies.
Currently supports trading on Bittrex exchange.
Simple High frequency trading bot for crypto currencies designed to
support multi exchanges and be controlled via Telegram.
This software is for educational purposes only.
Don't risk money which you are afraid to lose.
![freqtrade](https://raw.githubusercontent.com/gcarq/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
The command interface is accessible via Telegram (not required).
Just register a new bot on https://telegram.me/BotFather
and enter the telegram `token` and your `chat_id` in `config.json`
## Disclaimer
This software is for educational purposes only. Do not risk money which
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
Persistence is achieved through sqlite.
Always start by running a trading bot in Dry-run and do not engage money
before you understand how it works and what profit/loss you should
expect.
### Telegram RPC commands:
* /start: Starts the trader
* /stop: Stops the trader
* /status [table]: Lists all open trades
* /count: Displays number of open trades
* /profit: Lists cumulative profit from all finished trades
* /forcesell <trade_id>|all: Instantly sells the given trade (Ignoring `minimum_roi`).
* /performance: Show performance of each finished trade grouped by pair
* /balance: Show account balance per currency
* /help: Show help message
* /version: Show version
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.
### Config
`minimal_roi` is a JSON object where the key is a duration
in minutes and the value is the minimum ROI in percent.
See the example below:
## 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.
## Features
- [x] **Based on Python 3.6+**: For botting on any operating system -
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] **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.
### Exchange supported
- [x] Bittrex
- [ ] Binance
- [ ] Others
## 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.
The following steps are made for Linux/MacOS environment
**1. Clone the repo**
```bash
git clone git@github.com:gcarq/freqtrade.git
git checkout develop
cd freqtrade
```
"minimal_roi": {
"50": 0.0, # Sell after 30 minutes if the profit is not negative
"40": 0.01, # Sell after 25 minutes if there is at least 1% profit
"30": 0.02, # Sell after 15 minutes if there is at least 2% profit
"0": 0.045 # Sell immediately if there is at least 4.5% profit
},
**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 `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
`stoploss` is loss in percentage that should trigger a sale.
For example value `-0.10` will cause immediate sell if the
profit dips below -10% for a given trade. This parameter is optional.
`initial_state` is an optional field that defines the initial application state.
Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first.
### 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).
`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.
### [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.
The other values should be self-explanatory,
if not feel free to raise a github issue.
### [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.
### Prerequisites
* python3.6
* sqlite
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
### [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.
### Install
**Important:** Always create your PR against the `develop` branch, not
`master`.
#### Arch Linux
## Basic Usage
Use your favorite AUR helper and install `python-freqtrade-git`.
### Bot commands
#### Manually
```bash
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
[--dry-run-db]
{backtesting,hyperopt} ...
`master` branch contains the latest stable release.
Simple High Frequency Trading Bot for crypto currencies
`develop` branch has often new features, but might also cause breaking changes. To use it, you are encouraged to join our [slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
positional arguments:
{backtesting,hyperopt}
backtesting backtesting module
hyperopt hyperopt module
optional arguments:
-h, --help show this help message and exit
-c PATH, --config PATH
specify configuration file (default: config.json)
-v, --verbose be verbose
--version show program's version number and exit
--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.
```
$ cd freqtrade/
# copy example config. Dont forget to insert your api keys
$ cp config.json.example config.json
$ python -m venv .env
$ source .env/bin/activate
$ pip install -r requirements.txt
$ pip install -e .
$ ./freqtrade/main.py
```
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)
There is also an [article](https://www.sales4k.com/blockchain/high-frequency-trading-bot-tutorial/) about how to setup the bot (thanks [@gurghet](https://github.com/gurghet)).*
### 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)
\* *Note:* that article was written for an earlier version, so it may be outdated
- `/start`: Starts the trader
- `/stop`: Stops the trader
- `/status [table]`: Lists all open trades
- `/count`: Displays number of open trades
- `/profit`: Lists cumulative profit from all finished trades
- `/forcesell <trade_id>|all`: Instantly sells the given trade
(Ignoring `minimum_roi`).
- `/performance`: Show performance of each finished trade grouped by pair
- `/balance`: Show account balance per currency
- `/daily <n>`: Shows profit or loss per day, over the last n days
- `/help`: Show help message
- `/version`: Show version
#### Docker
## Requirements
Building the image:
### Min hardware required
To run this bot we recommend you a cloud instance with a minimum of:
* Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU
```
$ cd freqtrade
$ 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
a SQLite database file (see second example) to keep it between updates.
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):
```
$ docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
To run a restartable instance in the background (feel free to place your
configuration and database files wherever it feels comfortable on your
filesystem):
```
$ cd ~/.freq
$ touch tradesv3.sqlite
$ docker run -d \
--name freqtrade \
-v ~/.freq/config.json:/freqtrade/config.json \
-v ~/.freq/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
freqtrade
```
If you are using `dry_run=True` it's not necessary to mount `tradesv3.sqlite`.
You can then use the following commands to monitor and manage your container:
```
$ docker logs freqtrade
$ docker logs -f freqtrade
$ docker restart freqtrade
$ docker stop freqtrade
$ docker start freqtrade
```
You do not need to rebuild the image for configuration
changes, it will suffice to edit `config.json` and restart the container.
### Execute tests
```
$ pytest
```
This will by default skip the slow running backtest set. To run backtest set:
```
$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py
```
### Contributing
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
- Create your PR against the `develop` branch, not `master`.
- New features need to contain unit tests.
- If you are unsure, discuss the feature on [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.
### Software requirements
- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
- [pip](https://pip.pypa.io/en/stable/installing/)
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
- [Docker](https://www.docker.com/products/docker) (Recommended)

View File

@@ -2,14 +2,16 @@
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"fiat_display_currency": "USD",
"dry_run": false,
"minimal_roi": {
"50": 0.0,
"40": 0.01,
"30": 0.02,
"0": 0.045
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.40,
"stoploss": -0.10,
"unfilledtimeout": 600,
"bid_strategy": {
"ask_last_balance": 0.0
},
@@ -18,17 +20,25 @@
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_RLC",
"BTC_TKN",
"BTC_TRST",
"BTC_SWT",
"BTC_PIVX",
"BTC_MLN",
"BTC_XZC",
"BTC_TIME",
"BTC_LUN"
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
],
"pair_blacklist": [
"BTC_DOGE"
]
},
"experimental": {
"use_sell_signal": false,
"sell_profit_only": false
},
"telegram": {
"enabled": true,
"token": "token",

0
docs/.gitkeep Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

109
docs/backtesting.md Normal file
View File

@@ -0,0 +1,109 @@
# 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).
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 backtesting is very easy with freqtrade.
### Run a backtesting against the currencies listed in your config file
**With 5 min tickers (Per default)**
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation
```
**With 1 min tickers**
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1
```
**Reload your testdata files**
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
```
**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**
```bash
python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180101
```
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
```
The last line will give you the overall performance of your strategy,
here:
```
TOTAL 419 -0.41 -0.00348593 52.9
```
We understand the bot has made `419` trades for an average duration of
`52.9` min, with a performance of `-0.41%` (loss), that means it has
lost a total of `-0.00348593 BTC`.
As you will see your strategy performance will be influenced by your buy
strategy, your sell strategy, and also by the `minimal_roi` and
`stop_loss` you have set.
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
},
```
On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
(55%), there is a lot of chance that the bot will never reach this
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)

117
docs/bot-optimization.md Normal file
View File

@@ -0,0 +1,117 @@
# Bot Optimization
This page explains where to customize your strategies, and add new
indicators.
## Table of Contents
- [Change your strategy](#change-your-strategy)
- [Add more Indicator](#add-more-indicator)
## Change your strategy
The bot is using buy and sell strategies to buy and sell your trades.
Both are customizable.
### Buy strategy
The default buy strategy is located in the file
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73-L92).
Edit the function `populate_buy_trend()` to update your buy strategy.
Sample:
```python
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['rsi'] < 35) &
(dataframe['fastd'] < 35) &
(dataframe['adx'] > 30) &
(dataframe['plus_di'] > 0.5)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > 0.5)
),
'buy'] = 1
return dataframe
```
### Sell strategy
The default buy strategy is located in the file
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115)
Edit the function `populate_sell_trend()` to update your buy strategy.
Sample:
```python
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(
(crossed_above(dataframe['rsi'], 70)) |
(crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > 0.5)
),
'sell'] = 1
return dataframe
```
## Add more Indicator
As you have seen, buy and sell strategies need indicators. You can see
the indicators in the file
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L95-L115).
Of course you can add more indicators by extending the list contained in
the function `populate_indicators()`.
Sample:
```python
def populate_indicators(dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
"""
dataframe['sar'] = ta.SAR(dataframe)
dataframe['adx'] = ta.ADX(dataframe)
stoch = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch['fastd']
dataframe['fastk'] = stoch['fastk']
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
dataframe['mfi'] = ta.MFI(dataframe)
dataframe['rsi'] = ta.RSI(dataframe)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
dataframe['ao'] = awesome_oscillator(dataframe)
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
return dataframe
```
## 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).

139
docs/bot-usage.md Normal file
View File

@@ -0,0 +1,139 @@
# Bot usage
This page explains the difference parameters of the bot and how to run
it.
## Table of Contents
- [Bot commands](#bot-commands)
- [Backtesting commands](#backtesting-commands)
- [Hyperopt commands](#hyperopt-commands)
## Bot commands
```
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
[--dry-run-db]
{backtesting,hyperopt} ...
Simple High Frequency Trading Bot for crypto currencies
positional arguments:
{backtesting,hyperopt}
backtesting backtesting module
hyperopt hyperopt module
optional arguments:
-h, --help show this help message and exit
-c PATH, --config PATH
specify configuration file (default: config.json)
-v, --verbose be verbose
--version show program's version number and exit
-dd PATH, --datadir PATH
Path is from where backtesting and hyperopt will load the
ticker data files (default freqdata/tests/testdata).
--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.
```
### How to use a different config file?
The bot allows you to select which config file you want to use. Per
default, the bot will load the file `./config.json`
```bash
python3 ./freqtrade/main.py -c path/far/far/away/config.json
```
### How to use --dynamic-whitelist?
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
on BaseVolume. This value can be changed when you run the script.
**By Default**
Get the 20 currencies based on BaseVolume.
```bash
python3 ./freqtrade/main.py --dynamic-whitelist
```
**Customize the number of currencies to retrieve**
Get the 30 currencies based on BaseVolume.
```bash
python3 ./freqtrade/main.py --dynamic-whitelist 30
```
**Exception**
`--dynamic-whitelist` must be greater than 0. If you enter 0 or a
negative value (e.g -2), `--dynamic-whitelist` will use the default
value (20).
### How to use --dry-run-db?
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`
```bash
python3 ./freqtrade/main.py -c config.json --dry-run-db
```
## Backtesting commands
Backtesting also uses the config specified via `-c/--config`.
```
usage: freqtrade backtesting [-h] [-l] [-i INT] [--realistic-simulation]
[-r]
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)
--realistic-simulation
uses max_open_trades from config to simulate real
world limitations
-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.
```
### How to use --refresh-pairs-cached parameter?
The first time your run Backtesting, it will take the pairs you have
set in your config file and download data from Bittrex.
If for any reason you want to update your data set, you use
`--refresh-pairs-cached` to force Backtesting to update the data it has.
**Use it only if you want to update your data set. You will not be able
to come back to the previous version.**
To test your strategy with latest data, we recommend continuing using
the parameter `-l` or `--live`.
## Hyperopt commands
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`.
```
usage: freqtrade hyperopt [-h] [-e INT] [--use-mongodb]
optional arguments:
-h, --help show this help message and exit
-e INT, --epochs INT specify number of epochs (default: 100)
--use-mongodb parallelize evaluations with mongodb (requires mongod
in PATH)
```
## 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)
## 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).

128
docs/configuration.md Normal file
View File

@@ -0,0 +1,128 @@
# Configure the bot
This page explains how to configure your `config.json` file.
## Table of Contents
- [Bot commands](#bot-commands)
- [Backtesting commands](#backtesting-commands)
- [Hyperopt commands](#hyperopt-commands)
## Setup config.json
We recommend to copy and use the `config.json.example` as a template
for your bot configuration.
The table below will list all configuration parameters.
| Command | Default | Mandatory | Description |
|----------|---------|----------|-------------|
| `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.
| `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 | Yes | Set the threshold in percent the bot will use to sell a trade. More information below.
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below.
| `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.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`.
| `telegram.enabled` | true | Yes | Enable or not the usage of Telegram.
| `telegram.token` | token | No | Your Telegram bot token. Only required is `enable` is `true`.
| `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required is `enable` is `true`.
| `initial_state` | running | No | Defines the initial application state. More information below.
| `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).
### Understand minimal_roi
`minimal_roi` is a JSON object where the key is a duration
in minutes and the value is the minimum ROI in percent.
See the example below:
```
"minimal_roi": {
"40": 0.0, # Sell after 40 minutes if the profit is not negative
"30": 0.01, # Sell after 30 minutes if there is at least 1% profit
"20": 0.02, # Sell after 20 minutes if there is at least 2% profit
"0": 0.04 # Sell immediately if there is at least 4% profit
},
```
### Understand stoploss
`stoploss` is loss in percentage that should trigger a sale.
For example value `-0.10` will cause immediate sell if the
profit dips below -10% for a given trade. This parameter is optional.
### Understand initial_state
`initial_state` is an optional field that defines the initial application state.
Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first.
### 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 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".
## Switch to dry-run mode
We recommend starting the bot in dry-run mode to see how your bot will
behave and how is the performance of your strategy. In Dry-run mode the
bot does not engage your money. It only runs a live simulation without
creating trades.
### To switch your bot in Dry-run mode:
1. Edit your `config.json` file
2. Switch dry-run to true
```json
"dry_run": true,
```
3. Remove your Bittrex API key (change them by fake api credentials)
```json
"exchange": {
"name": "bittrex",
"key": "key",
"secret": "secret",
...
}
```
Once you will be happy with your bot performance, you can switch it to
production mode.
## Switch to production mode
In production mode, the bot will engage your money. Be careful a wrong
strategy can lose all your money. Be aware of what you are doing when
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
```json
"dry_run": false,
```
3. Insert your Bittrex 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).
## 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).

21
docs/faq.md Normal file
View File

@@ -0,0 +1,21 @@
# freqtrade FAQ
#### I have waited 5 minutes, why hasn't the bot made any trades yet?!
Depending on the buy strategy, the amount of whitelisted coins, the situation of the market etc, it can take up to hours to find good entry position for a trade. Be patient!
#### I have made 12 trades already, why is my total profit negative?!
I understand your disappointment but unfortunately 12 trades is just not enough to say anything. If you run backtesting, you can see that our current algorithm does leave you on the plus side, but that is after thousands of trades and even there, you will be left with losses on specific coins that you have traded tens if not hundreds of times. We of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades.
#### Id like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again?
Not quite. Trades are persisted to a database but the configuration is currently only read when the bot is killed and restarted. `/stop` more like pauses. You can stop your bot, adjust settings and start it again.
#### 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]].
#### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
You can use the `/forcesell all` command from Telegram.

294
docs/hyperopt.md Normal file
View File

@@ -0,0 +1,294 @@
# Hyperopt
This page explains how to tune your strategy by finding the optimal
parameters with Hyperopt.
## Table of Contents
- [Prepare your Hyperopt](#prepare-hyperopt)
- [1. Configure your Guards and Triggers](#1-configure-your-guards-and-triggers)
- [2. Update the hyperopt config file](#2-update-the-hyperopt-config-file)
- [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
out Hyperopt file
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
### 1. Configure your Guards and Triggers
There are two places you need to change to add a new buy strategy for
testing:
- Inside the [populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L167-L207).
- Inside the [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94).
There you have two different type of indicators: 1. `guards` and 2.
`triggers`.
1. Guards are conditions like "never buy if ADX < 10", or never buy if
current price is over EMA10.
2. Triggers are ones that actually trigger buy in specific moment, like
"buy when EMA5 crosses over EMA10" or buy when close price touches lower
bollinger band.
HyperOpt will, for each eval round, pick just ONE trigger, and possibly
multiple guards. So that the constructed strategy will be something like
"*buy exactly when close price touches lower bollinger band, BUT only if
ADX > 10*".
If you have updated the buy strategy, means change the content of
`populate_buy_trend()` function you have to update the `guards` and
`triggers` hyperopts must used.
As for an example if your `populate_buy_trend()` function is:
```python
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(dataframe['rsi'] < 35) &
(dataframe['adx'] > 65),
'buy'] = 1
return dataframe
```
Your hyperopt file must contains `guards` to find the right value for
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
means you will need to enable/disable triggers.
In our case the `SPACE` and `populate_buy_trend` in hyperopt.py file
will be look like:
```python
SPACE = {
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'trigger': hp.choice('trigger', [
{'type': 'lower_bb'},
{'type': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]),
}
...
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
# TRIGGERS
triggers = {
'lower_bb': dataframe['tema'] <= dataframe['blower'],
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
}
...
```
### 2. Update the hyperopt config file
Hyperopt is using a dedicated config file. At this moment hyperopt
cannot use your config file. It is also made on purpose to allow you
testing your strategy with different configurations.
The Hyperopt configuration is located in
[freqtrade/optimize/hyperopt_conf.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/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 [SPACE dict](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L47-L94).
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
If it's a guard, you will add a line like this:
```
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
```
This says, "*one of guards is RSI, it can have two values, enabled or
disabled. If it is enabled, try different values for it between 20 and 40*".
So, the part of the strategy builder using the above setting looks like
this:
```
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
```
It checks if Hyperopt wants the RSI guard to be enabled for this
round `params['rsi']['enabled']` and if it is, then it will add a
condition that says RSI must be < than the value hyperopt picked
for this evaluation, that is given in the `params['rsi']['value']`.
That's it. Now you can add new parts of strategies to Hyperopt and it
will try all the combinations with all different values in the search
for best working algo.
### Add a new Indicators
If you want to test an indicator that isn't used by the bot currently,
you need to add it to
[freqtrade/analyze.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L40-L70)
inside the `populate_indicators` function.
## Execute Hyperopt
Once you have updated your hyperopt configuration you can run it.
Because hyperopt tries a lot of combination to find the best parameters
it will take time you will have the result (more than 30 mins).
We strongly recommend to use `screen` to prevent any connection loss.
```bash
python3 ./freqtrade/main.py -c config.json hyperopt
```
### Execute hyperopt with different ticker-data source
If you would like to learn parameters using an alternate ticke-data that
you have on-disk, use the --datadir PATH option. Default hyperopt will
use data from directory freqtrade/tests/testdata.
### 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:
```
Best parameters:
{
"adx": {
"enabled": true,
"value": 15.0
},
"fastd": {
"enabled": true,
"value": 40.0
},
"green_candle": {
"enabled": true
},
"mfi": {
"enabled": false
},
"over_sar": {
"enabled": false
},
"rsi": {
"enabled": true,
"value": 37.0
},
"trigger": {
"type": "lower_bb"
},
"uptrend_long_ema": {
"enabled": true
},
"uptrend_short_ema": {
"enabled": false
},
"uptrend_sma": {
"enabled": false
}
}
Best Result:
2197 trades. Avg profit 1.84%. Total profit 0.79367541 BTC. Avg duration 241.0 mins.
```
You should understand this result like:
- You should **consider** the guard "adx" (`"adx"` is `"enabled": true`)
and the best value is `15.0` (`"value": 15.0,`)
- You should **consider** the guard "fastd" (`"fastd"` is `"enabled":
true`) and the best value is `40.0` (`"value": 40.0,`)
- You should **consider** to enable the guard "green_candle"
(`"green_candle"` is `"enabled": true`) but this guards as no
customizable value.
- You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`)
- and so on...
You have to look from
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L170-L200)
what those values match to.
So for example you had `adx:` with the `value: 15.0` so we would look
at `adx`-block from
[freqtrade/optimize/hyperopt.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L178-L179).
That translates to the following code block to
[analyze.populate_buy_trend()](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/analyze.py#L73)
```
(dataframe['adx'] > 15.0)
```
So translating your whole hyperopt result to as the new buy-signal
would be the following:
```
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
(dataframe['adx'] > 15.0) & # adx-value
(dataframe['fastd'] < 40.0) & # fastd-value
(dataframe['close'] > dataframe['open']) & # green_candle
(dataframe['rsi'] < 37.0) & # rsi-value
(dataframe['ema50'] > dataframe['ema100']) # uptrend_long_ema
),
'buy'] = 1
return 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).

32
docs/index.md Normal file
View File

@@ -0,0 +1,32 @@
# freqtrade documentation
Welcome to freqtrade documentation. Please feel free to contribute to
this documentation if you see it became outdated by sending us a
Pull-request. Do not hesitate to reach us on
[Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
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)

246
docs/installation.md Normal file
View File

@@ -0,0 +1,246 @@
# Install the bot
This page explains how to prepare your environment for running the bot.
To understand how to set up the bot please read the Bot
[Bot configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
page.
## Table of Contents
- [Docker Automatic Installation](#docker)
- [Linux or Mac manual Installation](#linux--mac)
- [Linux - Ubuntu 16.04](#21-linux---ubuntu-1604)
- [Linux - Other distro](#22-linux---other-distro)
- [MacOS installation](#23-macos-installation)
- [Advanced Linux ](#advanced-linux)
- [Windows manual Installation](#windows)
# Docker
## Easy installation
Start by downloading Docker for your platform:
- [Mac](https://www.docker.com/products/docker#/mac)
- [Windows](https://www.docker.com/products/docker#/windows)
- [Linux](https://www.docker.com/products/docker#/linux)
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. Clone the git
```bash
git clone https://github.com/gcarq/freqtrade.git
```
2. (Optional) Checkout the develop branch
```bash
git checkout develop
```
3. Go into the new directory
```bash
cd freqtrade
```
4. Copy `config.sample` to `config.json`
```bash
cp 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
5. Create your DB file (Optional, the bot will create it if it is missing)
```bash
# For Production
touch tradesv3.sqlite
# For Dry-run
touch tradesv3.dryrun.sqlite
```
### 2. Build the docker image
```bash
cd freqtrade
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
a sqlite database file (see the "5. Run a restartable docker image"
section) to keep it between updates.
### 3. Verify the docker image
After 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):
```
docker run --rm -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.
### 5. Run a restartable docker image
To run a restartable instance in the background (feel free to place your
configuration and database files wherever it feels comfortable on your
filesystem).
**5.1. Move your config file and database**
```bash
mkdir ~/.freqtrade
mv config.json ~/.freqtrade
mv tradesv3.sqlite ~/.freqtrade
```
**5.2. Run the docker image**
```bash
docker run -d \
--name freqtrade \
-v ~/.freqtrade/config.json:/freqtrade/config.json \
-v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
freqtrade
```
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`.
### 6. Monitor your Docker instance
You can then use the following commands to monitor and manage your container:
```bash
docker logs freqtrade
docker logs -f freqtrade
docker restart freqtrade
docker stop freqtrade
docker start freqtrade
```
You do not need to rebuild the image for configuration changes, it will
suffice to edit `config.json` and restart the container.
# Linux / MacOS
## 1. 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)
## 2. First install required packages
This bot require Python 3.6 and TA-LIB
### 2.1 Linux - Ubuntu 16.04
**2.1.1. Install Python 3.6, Git, and wget**
```bash
sudo add-apt-repository ppa:jonathonf/python-3.6
sudo apt-get update
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
```
**2.1.2. Install TA-LIB**
Official webpage: https://mrjbq7.github.io/ta-lib/install.html
```
wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
tar xvzf ta-lib-0.4.0-src.tar.gz
cd ta-lib
./configure --prefix=/usr
make
make install
cd ..
rm -rf ./ta-lib*
```
**2.1.3. [Optional] Install MongoDB**
Install MongoDB if you plan to optimize your strategy with Hyperopt.
```bash
sudo apt-get install mongodb-org
```
Complete tutorial on [Digital Ocean: How to Install MongoDB on Ubuntu 16.04](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-16-04)
### 2.2. Linux - Other distro
If you are on a different Linux OS you maybe have to adapt things like:
- package manager (for example yum instead of apt-get)
- package names
### 2.3. MacOS installation
**2.3.1. Install Python 3.6, git and wget**
```bash
brew install python3 git wget
```
**2.3.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. Clone the repo
The following steps are made for Linux/mac environment
1. Clone the git `git clone https://github.com/gcarq/freqtrade.git`
2. (Optional) Checkout the develop branch `git checkout develop`
## 4. Prepare the bot
```bash
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)
## 5. Setup your virtual env
```bash
python3.6 -m venv .env
source .env/bin/activate
pip3.6 install -r requirements.txt
pip3.6 install -e .
```
## 6. Run the bot
If this is the first time you run the bot, ensure you are running it
in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
```bash
python3.6 ./freqtrade/main.py -c config.json
```
### Advanced Linux
**systemd service file**
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
```
# Windows
We do recommend Windows users to use [Docker](#docker) this will work
much easier and smoother (also safer).
```cmd
#copy paste config.json to \path\freqtrade-develop\freqtrade
>cd \path\freqtrade-develop
>python -m venv .env
>cd .env\Scripts
>activate.bat
>cd \path\freqtrade-develop
>pip install -r requirements.txt
>pip install -e .
>cd freqtrade
>python main.py
```
*Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/gcarq/freqtrade/issues/222)*
## Next step
Now you have an environment ready, the next step is to
[configure your bot](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md).

46
docs/pre-requisite.md Normal file
View File

@@ -0,0 +1,46 @@
# Pre-requisite
Before running your bot in production you will need to setup few
external API. In production mode, the bot required valid Bittrex API
credentials and a Telegram bot (optional but recommended).
## Table of Contents
- [Setup your Bittrex account](#setup-your-bittrex-account)
- [Backtesting commands](#setup-your-telegram-bot)
## Setup your Bittrex account
*To be completed, please feel free to complete this section.*
## Setup your Telegram bot
The only things you need is a working Telegram bot and its API token.
Below we explain how to create your Telegram Bot, and how to get your
Telegram user id.
### 1. Create your instagram bot
**1.1. Start a chat with https://telegram.me/BotFather**
**1.2. Send the message** `/newbot`
*BotFather response:*
```
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
```
**1.3. Choose the public name of your bot (e.g "`Freqtrade bot`")**
*BotFather response:*
```
Good. Now let's choose a username for your bot. It must end in `bot`. Like this, for example: TetrisBot or tetris_bot.
```
**1.4. Choose the name id of your bot (e.g "`My_own_freqtrade_bot`")**
**1.5. Father bot will return you the token (API key)**
Copy it and keep it you will use it for the config parameter `token`.
*BotFather response:*
```
Done! Congratulations on your new bot. You will find it at t.me/My_own_freqtrade_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.
Use this token to access the HTTP API:
521095879:AAEcEZEL7ADJ56FtG_qD0bQJSKETbXCBCi0
For a description of the Bot API, see this page: https://core.telegram.org/bots/api
```
### 2. Get your user id
**2.1. Talk to https://telegram.me/userinfobot**
**2.2. Get your "Id", you will use it for the config parameter
`chat_id`.**

78
docs/sql_cheatsheet.md Normal file
View File

@@ -0,0 +1,78 @@
# SQL Helper
This page constains some help if you want to edit your sqlite db.
## Install sqlite3
**Ubuntu/Debian installation**
```bash
sudo apt-get install sqlite3
```
## Open the DB
```bash
sqlite3
.open <filepath>
```
## Table structure
### List tables
```bash
.tables
```
### Display table structure
```bash
.schema <table_name>
```
### Trade table structure
```sql
CREATE TABLE 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))
);
```
## Get all trades in the table
```sql
SELECT * FROM trades;
```
## Fix trade still open after a /forcesell
```sql
UPDATE trades
SET is_open=0, close_date=<close_date>, close_rate=<close_rate>, close_profit=close_rate/open_rate
WHERE id=<trade_ID_to_update>;
```
**Example:**
```sql
UPDATE trades
SET is_open=0, close_date='2017-12-20 03:08:45.103418', close_rate=0.19638016, close_profit=0.0496
WHERE id=31;
```
## Fix wrong fees in the table
If your DB was created before
[PR#200](https://github.com/gcarq/freqtrade/pull/200) was merged
(before 12/23/17).
```sql
UPDATE trades SET fee=0.0025 WHERE fee=0.005;
```

129
docs/telegram-usage.md Normal file
View File

@@ -0,0 +1,129 @@
# Telegram usage
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)
and add your Telegram API keys into your config file.
## Telegram commands
Per default, the Telegram bot shows predefined commands. Some commands
are only available by sending them to the bot. The table below list the
official commands. You can ask at any moment for help with `/help`.
| Command | Default | Description |
|----------|---------|-------------|
| `/start` | | Starts the trader
| `/stop` | | Starts the trader
| `/status` | | Lists all open trades
| `/status table` | | List all open trades in a table format
| `/count` | | Displays number of trades used and available
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
| `/forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
| `/performance` | | Show performance of each finished trade grouped by pair
| `/balance` | | Show account balance per currency
| `/daily <n>` | 7 | Shows profit or loss per day, over the last n days
| `/help` | | Show help message
| `/version` | | Show version
## Telegram commands in action
Below, example of Telegram message you will receive for each command.
### /start
> **Status:** `running`
### /stop
> `Stopping trader ...`
> **Status:** `stopped`
## /status
For each open trade, the bot will send you the following message.
> **Trade ID:** `123`
> **Current Pair:** BTC_CVC
> **Open Since:** `1 days ago`
> **Amount:** `26.64180098`
> **Open Rate:** `0.00007489`
> **Close Rate:** `None`
> **Current Rate:** `0.00007489`
> **Close Profit:** `None`
> **Current Profit:** `12.95%`
> **Open Order:** `None`
## /status table
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%
```
## /count
Return the number of trades used and available.
```
current max
--------- -----
2 10
```
## /profit
Return a summary of your profit/loss and performance.
> **ROI:** Close trades
> ∙ `0.00485701 BTC (258.45%)`
> ∙ `62.968 USD`
> **ROI:** All trades
> ∙ `0.00255280 BTC (143.43%)`
> ∙ `33.095 EUR`
>
> **Total Trade Count:** `138`
> **First Trade opened:** `3 days ago`
> **Latest Trade opened:** `2 minutes ago`
> **Avg. Duration:** `2:33:45`
> **Best Performing:** `BTC_PAY: 50.23%`
## /forcesell <trade_id>
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
## /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%`
> ...
## /balance
Return the balance of all crypto-currency your have on the exchange.
> **Currency:** BTC
> **Available:** 3.05890234
> **Balance:** 3.05890234
> **Pending:** 0.0
> **Currency:** CVC
> **Available:** 86.64180098
> **Balance:** 86.64180098
> **Pending:** 0.0
## /daily <n>
Per default `/daily` will return the 7 last days.
The example below if for `/daily 3`:
> **Daily Profit over the last 3 days:**
```
Day Profit BTC Profit USD
---------- -------------- ------------
2018-01-03 0.00224175 BTC 29,142 USD
2018-01-02 0.00033131 BTC 4,307 USD
2018-01-01 0.00269130 BTC 34.986 USD
```
## /version
> **Version:** `0.14.3`

14
freqtrade.service Normal file
View File

@@ -0,0 +1,14 @@
[Unit]
Description=Freqtrade Daemon
After=network.target
[Service]
# Set WorkingDirectory and ExecStart to your file paths accordingly
# NOTE: %h will be resolved to /home/<username>
WorkingDirectory=%h/freqtrade
ExecStart=/usr/bin/freqtrade --dynamic-whitelist 40
Restart=on-failure
[Install]
WantedBy=default.target

View File

@@ -1,3 +1,16 @@
__version__ = '0.14.2'
""" FreqTrade bot """
__version__ = '0.16.0'
from . import main
class DependencyException(BaseException):
"""
Indicates that a assumed dependency is not met.
This could happen when there is currently not enough money on the account.
"""
class OperationalException(BaseException):
"""
Requires manual intervention.
This happens when an exchange returns an unexpected error during runtime.
"""

View File

@@ -1,16 +1,27 @@
"""
Functions to analyze ticker data with indicators and produce buy and sell signals
"""
import logging
from datetime import timedelta
from enum import Enum
from typing import Dict, List
import arrow
import talib.abstract as ta
from pandas import DataFrame, to_datetime
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.exchange import get_ticker_history
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
logger = logging.getLogger(__name__)
class SignalType(Enum):
""" Enum to distinguish between buy and sell signals """
BUY = "buy"
SELL = "sell"
def parse_ticker_dataframe(ticker: list) -> DataFrame:
"""
Analyses the trend for the given ticker history
@@ -29,74 +40,266 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
def populate_indicators(dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
"""
dataframe['sar'] = ta.SAR(dataframe)
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
stoch = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch['fastd']
dataframe['fastk'] = stoch['fastk']
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
dataframe['mfi'] = ta.MFI(dataframe)
dataframe['rsi'] = ta.RSI(dataframe)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
dataframe['ao'] = awesome_oscillator(dataframe)
# Awesome oscillator
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
"""
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
dataframe['cci'] = ta.CCI(dataframe)
"""
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# Minus Directional Indicator / Movement
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
"""
# ROC
dataframe['roc'] = ta.ROC(dataframe)
"""
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
"""
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
"""
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
"""
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
"""
# Overlap Studies
# ------------------------------------
# Previous Bollinger bands
# Because ta.BBANDS implementation is broken with small numbers, it actually
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
# and use middle band instead.
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
"""
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
"""
# EMA - Exponential Moving Average
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
"""
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
"""
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
"""
return dataframe
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy trend for the given dataframe
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.ix[
(dataframe['close'] < dataframe['sma']) &
(dataframe['tema'] <= dataframe['blower']) &
(dataframe['mfi'] < 25) &
(dataframe['fastd'] < 25) &
(dataframe['adx'] > 30),
dataframe.loc[
(
(dataframe['rsi'] < 35) &
(dataframe['fastd'] < 35) &
(dataframe['adx'] > 30) &
(dataframe['plus_di'] > 0.5)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > 0.5)
),
'buy'] = 1
dataframe.ix[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe
def analyze_ticker(pair: str) -> DataFrame:
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
"""
Get ticker data for given currency pair, push it to a DataFrame and
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
(qtpylib.crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > 0.5)
),
'sell'] = 1
return dataframe
def analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
"""
Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
"""
dataframe = parse_ticker_dataframe(ticker_history)
dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe)
dataframe = populate_sell_trend(dataframe)
return dataframe
def get_signal(pair: str, signal: SignalType) -> bool:
"""
Calculates current signal based several technical analysis indicators
:param pair: pair in format BTC_ANT or BTC-ANT
:return: True if pair is good for buying, False otherwise
"""
ticker_hist = get_ticker_history(pair)
if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
return DataFrame()
return False
dataframe = parse_ticker_dataframe(ticker_hist)
dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe)
return dataframe
try:
dataframe = analyze_ticker(ticker_hist)
except ValueError as ex:
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
return False
except Exception as ex:
logger.exception('Unexpected error when analyzing ticker for pair %s: %s', pair, str(ex))
return False
def get_buy_signal(pair: str) -> bool:
"""
Calculates a buy signal based several technical analysis indicators
:param pair: pair in format BTC_ANT or BTC-ANT
:return: True if pair is good for buying, False otherwise
"""
dataframe = analyze_ticker(pair)
if dataframe.empty:
return False
@@ -107,6 +310,6 @@ def get_buy_signal(pair: str) -> bool:
if signal_date < arrow.now() - timedelta(minutes=10):
return False
signal = latest['buy'] == 1
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
return signal
result = latest[signal.value] == 1
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
return result

View File

@@ -1,11 +1,15 @@
# pragma pylint: disable=W0603
""" Cryptocurrency Exchanges support """
import enum
import logging
from random import randint
from typing import List, Dict, Any, Optional
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
@@ -48,7 +52,7 @@ def init(config: dict) -> None:
try:
exchange_class = Exchanges[name.upper()].value
except KeyError:
raise RuntimeError('Exchange {} is not supported'.format(name))
raise OperationalException('Exchange {} is not supported'.format(name))
_API = exchange_class(exchange_config)
@@ -59,25 +63,31 @@ def init(config: dict) -> None:
def validate_pairs(pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
Raises RuntimeError if one pair is not available.
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
stake_cur = _CONF['stake_currency']
for pair in pairs:
if not pair.startswith(stake_cur):
raise RuntimeError(
raise OperationalException(
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
)
if pair not in markets:
raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower()))
raise OperationalException(
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
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, 1e6))
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
@@ -95,7 +105,7 @@ def buy(pair: str, rate: float, amount: float) -> str:
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, 1e6))
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
@@ -124,8 +134,8 @@ def get_balances():
return _API.get_balances()
def get_ticker(pair: str) -> dict:
return _API.get_ticker(pair)
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
return _API.get_ticker(pair, refresh)
@cached(TTLCache(maxsize=100, ttl=30))

View File

@@ -1,8 +1,12 @@
import logging
from typing import List, Dict
import requests
from typing import Dict, List, Optional
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
from bittrex.bittrex import Bittrex as _Bittrex
from bittrex.bittrex import API_V1_1, API_V2_0
from requests.exceptions import ContentDecodingError
from freqtrade import OperationalException
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
@@ -11,6 +15,20 @@ _API: _Bittrex = None
_API_V2: _Bittrex = None
_EXCHANGE_CONF: dict = {}
# API socket timeout
API_TIMEOUT = 60
def custom_requests(request_url, apisign):
"""
Set timeout for requests
"""
return requests.get(
request_url,
headers={"apisign": apisign},
timeout=API_TIMEOUT
).json()
class Bittrex(Exchange):
"""
@@ -29,23 +47,40 @@ class Bittrex(Exchange):
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V1_1,
dispatch=custom_requests
)
_API_V2 = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V2_0,
dispatch=custom_requests
)
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('Got {}'.format(response['message']))
@property
def fee(self) -> float:
# See https://bittrex.com/fees
# 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']:
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
@@ -55,7 +90,8 @@ class Bittrex(Exchange):
def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
@@ -65,7 +101,8 @@ class Bittrex(Exchange):
def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency)
if not data['success']:
raise RuntimeError('{message} params=({currency})'.format(
Bittrex._validate_response(data)
raise OperationalException('{message} params=({currency})'.format(
message=data['message'],
currency=currency))
return float(data['result']['Balance'] or 0.0)
@@ -73,24 +110,33 @@ class Bittrex(Exchange):
def get_balances(self):
data = _API.get_balances()
if not data['success']:
raise RuntimeError('{message}'.format(message=data['message']))
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return data['result']
def get_ticker(self, pair: str) -> dict:
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']:
raise RuntimeError('{message} params=({pair})'.format(
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
if not data['result']['Bid'] or not data['result']['Ask'] or not data['result']['Last']:
raise RuntimeError('{message} params=({pair})'.format(
message=data['message'],
if not data.get('result') \
or not data['result'].get('Bid') \
or not data['result'].get('Ask') \
or not data['result'].get('Last'):
raise ContentDecodingError('{message} params=({pair})'.format(
message='Got invalid response from bittrex',
pair=pair))
return {
# 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:
@@ -104,16 +150,20 @@ class Bittrex(Exchange):
# These sanity check are necessary because bittrex cannot keep their API stable.
if not data.get('result'):
return []
raise ContentDecodingError('{message} params=({pair})'.format(
message='Got invalid response from bittrex',
pair=pair))
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
for tick in data['result']:
if prop not in tick.keys():
logger.warning('Required property %s not present in response', prop)
return []
raise ContentDecodingError('{message} params=({pair})'.format(
message='Required property {} not present in response'.format(prop),
pair=pair))
if not data['success']:
raise RuntimeError('{message} params=({pair})'.format(
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
@@ -122,7 +172,8 @@ class Bittrex(Exchange):
def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id)
if not data['success']:
raise RuntimeError('{message} params=({order_id})'.format(
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
data = data['result']
@@ -140,7 +191,8 @@ class Bittrex(Exchange):
def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id)
if not data['success']:
raise RuntimeError('{message} params=({order_id})'.format(
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
@@ -150,19 +202,22 @@ class Bittrex(Exchange):
def get_markets(self) -> List[str]:
data = _API.get_markets()
if not data['success']:
raise RuntimeError('{message}'.format(message=data['message']))
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=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']:
raise RuntimeError('{message}'.format(message=data['message']))
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return data['result']
def get_wallet_health(self) -> List[Dict]:
data = _API_V2.get_wallet_health()
if not data['success']:
raise RuntimeError('{message}'.format(message=data['message']))
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return [{
'Currency': entry['Health']['Currency'],
'IsActive': entry['Health']['IsActive'],

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import List, Dict
from typing import Dict, List, Optional
class Exchange(ABC):
@@ -62,10 +62,11 @@ class Exchange(ABC):
"""
@abstractmethod
def get_ticker(self, pair: str) -> dict:
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,

163
freqtrade/fiat_convert.py Normal file
View File

@@ -0,0 +1,163 @@
import logging
import time
from pymarketcap import Pymarketcap
logger = logging.getLogger(__name__)
class CryptoFiat():
# Constants
CACHE_DURATION = 6 * 60 * 60 # 6 hours
def __init__(self, crypto_symbol: str, fiat_symbol: str, price: float) -> None:
"""
Create an object that will contains the price for a crypto-currency in fiat
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:param price: Price in FIAT
"""
# Public attributes
self.crypto_symbol = None
self.fiat_symbol = None
self.price = 0.0
# Private attributes
self._expiration = 0
self.crypto_symbol = crypto_symbol.upper()
self.fiat_symbol = fiat_symbol.upper()
self.set_price(price=price)
def set_price(self, price: float) -> None:
"""
Set the price of the Crypto-currency in FIAT and set the expiration time
:param price: Price of the current Crypto currency in the fiat
:return: None
"""
self.price = price
self._expiration = time.time() + self.CACHE_DURATION
def is_expired(self) -> bool:
"""
Return if the current price is still valid or needs to be refreshed
:return: bool, true the price is expired and needs to be refreshed, false the price is
still valid
"""
return self._expiration - time.time() <= 0
class CryptoToFiatConverter():
# 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"
]
def __init__(self) -> None:
try:
self._coinmarketcap = Pymarketcap()
except BaseException:
self._coinmarketcap = None
self._pairs = []
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
"""
Convert an amount of crypto-currency to fiat
:param crypto_amount: amount of crypto-currency to convert
:param crypto_symbol: crypto-currency used
:param fiat_symbol: fiat to convert to
:return: float, value in fiat of the crypto-currency amount
"""
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
return float(crypto_amount) * float(price)
def get_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
"""
Return the price of the Crypto-currency in Fiat
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:return: Price in FIAT
"""
crypto_symbol = crypto_symbol.upper()
fiat_symbol = fiat_symbol.upper()
# 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))
# Get the pair that interest us and return the price in fiat
for pair in self._pairs:
if pair.crypto_symbol == crypto_symbol and pair.fiat_symbol == fiat_symbol:
# If the price is expired we refresh it, avoid to call the API all the time
if pair.is_expired():
pair.set_price(
price=self._find_price(
crypto_symbol=pair.crypto_symbol,
fiat_symbol=pair.fiat_symbol
)
)
# return the last price we have for this pair
return pair.price
# The pair does not exist, so we create it and return the price
return self._add_pair(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol,
price=self._find_price(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol
)
)
def _add_pair(self, crypto_symbol: str, fiat_symbol: str, price: float) -> float:
"""
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:return: price in FIAT
"""
self._pairs.append(
CryptoFiat(
crypto_symbol=crypto_symbol,
fiat_symbol=fiat_symbol,
price=price
)
)
return price
def _is_supported_fiat(self, fiat: str) -> bool:
"""
Check if the FIAT your want to convert to is supported
:param fiat: FIAT to check (e.g USD)
:return: bool, True supported, False not supported
"""
fiat = fiat.upper()
return fiat in self.SUPPORTED_FIAT
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
"""
Call CoinMarketCap API to retrieve the price in the FIAT
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
:return: float, price of the crypto-currency in Fiat
"""
# 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))
try:
return float(
self._coinmarketcap.ticker(
currency=crypto_symbol,
convert=fiat_symbol
)['price_' + fiat_symbol.lower()]
)
except BaseException:
return 0.0

View File

@@ -2,82 +2,91 @@
import copy
import json
import logging
import sys
import time
import traceback
from datetime import datetime
from signal import signal, SIGINT, SIGABRT, SIGTERM
from typing import Dict, Optional, List
from typing import Dict, List, Optional
import arrow
import requests
from cachetools import cached, TTLCache
from jsonschema import validate
from freqtrade import __version__, exchange, persistence
from freqtrade.analyze import get_buy_signal
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state, build_arg_parser, throttle
from freqtrade import (DependencyException, OperationalException, __version__,
exchange, persistence, rpc)
from freqtrade.analyze import SignalType, get_signal
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.misc import (State, get_state, load_config, parse_args,
throttle, update_state)
from freqtrade.persistence import Trade
from freqtrade.rpc import telegram
logger = logging.getLogger('freqtrade')
_CONF = {}
def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None:
def refresh_whitelist(whitelist: List[str]) -> List[str]:
"""
Check wallet health and remove pair from whitelist if necessary
:param whitelist: a new whitelist (optional)
:return: None
: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
"""
whitelist = whitelist or _CONF['exchange']['pair_whitelist']
sanitized_whitelist = []
sanitized_whitelist = whitelist
health = exchange.get_wallet_health()
known_pairs = set()
for status in health:
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
if pair not in whitelist:
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
if pair not in whitelist or pair in _CONF['exchange'].get('pair_blacklist', []):
continue
if status['IsActive']:
sanitized_whitelist.append(pair)
else:
# else the pair is valid
known_pairs.add(pair)
# Market is not active
if not status['IsActive']:
sanitized_whitelist.remove(pair)
logger.info(
'Ignoring %s from whitelist (reason: %s).',
pair, status.get('Notice') or 'wallet is not active'
)
if _CONF['exchange']['pair_whitelist'] != sanitized_whitelist:
logger.debug('Using refreshed pair whitelist: %s ...', sanitized_whitelist)
_CONF['exchange']['pair_whitelist'] = sanitized_whitelist
# 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 _process(dynamic_whitelist: Optional[bool] = False) -> bool:
def _process(nb_assets: Optional[int] = 0) -> bool:
"""
Queries the persistence layer for open trades and handles them,
otherwise a new trade is created.
:param: dynamic_whitelist: True is a dynamic whitelist should be generated (optional)
:param: nb_assets: the maximum number of pairs to be traded at the same time
:return: True if a trade has been created or closed, False otherwise
"""
state_changed = False
try:
# Refresh whitelist based on wallet maintenance
refresh_whitelist(
gen_pair_whitelist(_CONF['stake_currency']) if dynamic_whitelist else None
sanitized_list = refresh_whitelist(
gen_pair_whitelist(
_CONF['stake_currency']
) if nb_assets else _CONF['exchange']['pair_whitelist']
)
# Keep only the subsets of pairs wanted (up to nb_assets)
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
_CONF['exchange']['pair_whitelist'] = final_list
# Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < _CONF['max_open_trades']:
try:
# Create entity and execute trade
trade = create_trade(float(_CONF['stake_amount']))
if trade:
Trade.session.add(trade)
state_changed = True
else:
state_changed = create_trade(float(_CONF['stake_amount']))
if not state_changed:
logger.info(
'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...'
)
except ValueError:
logger.exception('Unable to create trade')
except DependencyException as exception:
logger.warning('Unable to create trade: %s', exception)
for trade in trades:
# Get order details for actual price per unit
@@ -86,44 +95,73 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
logger.info('Got open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
if not close_trade_if_fulfilled(trade):
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
state_changed = handle_trade(trade) or state_changed
if 'unfilledtimeout' in _CONF:
# Check and handle any timed out open orders
check_handle_timedout(_CONF['unfilledtimeout'])
Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__)
logger.exception(msg)
logger.warning(
'Got %s in _process(), retrying in 30 seconds...',
error
)
time.sleep(30)
except RuntimeError:
telegram.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
except OperationalException:
rpc.send_msg('*Status:* Got OperationalException:\n```\n{traceback}```{hint}'.format(
traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.'
))
logger.exception('Got RuntimeError. Stopping trader ...')
logger.exception('Got OperationalException. Stopping trader ...')
update_state(State.STOPPED)
return state_changed
def close_trade_if_fulfilled(trade: Trade) -> bool:
def check_handle_timedout(timeoutvalue: int) -> None:
"""
Checks if the trade is closable, and if so it is being closed.
:param trade: Trade
:return: True if trade has been closed else False
Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out
:return: None
"""
# If we don't have an open order and the close rate is already set,
# we can close this trade.
if trade.close_profit is not None \
and trade.close_date is not None \
and trade.close_rate is not None \
and trade.open_order_id is None:
trade.is_open = False
logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
trade
)
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
order = exchange.get_order(trade.open_order_id)
ordertime = arrow.get(order['opened'])
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
# Buy timeout - cancel order
exchange.cancel_order(trade.open_order_id)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
else:
# if trade is partially complete, edit the stake details for the trade
# and close the order
trade.amount = order['amount'] - order['remaining']
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
# Sell timeout - cancel order and update trade
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
logger.info('Sell order timeout for %s.', trade)
return True
return False
else:
# TODO: figure out how to handle partially complete sell orders
pass
def execute_sell(trade: Trade, limit: float) -> None:
@@ -137,35 +175,64 @@ def execute_sell(trade: Trade, limit: float) -> None:
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
trade.open_order_id = order_id
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
message = '*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
trade.exchange,
trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair),
limit,
fmt_exp_profit
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit)
message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format(
exchange=trade.exchange,
pair=trade.pair.replace('_', '/'),
pair_url=exchange.get_pair_detail_url(trade.pair),
limit=limit
)
logger.info(message)
telegram.send_msg(message)
# For regular case, when the configuration exists
if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF:
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
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=_CONF['stake_currency'],
profit_fiat=profit_fiat,
fiat=_CONF['fiat_display_currency'],
)
# Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well
else:
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade
)
# Send the message
rpc.send_msg(message)
Trade.session.flush()
def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bool:
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
"""
Based an earlier trade and current price and configuration, decides whether bot should sell
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(current_rate)
current_profit = trade.calc_profit_percent(current_rate)
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
logger.debug('Stop loss hit.')
return True
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
# Check if time matches and current rate is above threshold
time_diff = (current_time - trade.open_date).total_seconds() / 60
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
if time_diff > float(duration) and current_profit > threshold:
return True
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0)
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', float(current_profit) * 100.0)
return False
@@ -179,9 +246,26 @@ def handle_trade(trade: Trade) -> bool:
logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid']
if should_sell(trade, current_rate, datetime.utcnow()):
# Check if minimal roi has been reached
if min_roi_reached(trade, current_rate, datetime.utcnow()):
logger.debug('Executing sell due to ROI ...')
execute_sell(trade, current_rate)
return True
# Experimental: Check if sell signal has been enabled and triggered
if _CONF.get('experimental', {}).get('use_sell_signal'):
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
if _CONF.get('experimental', {}).get('sell_profit_only'):
logger.debug('Checking if trade is profitable ...')
if trade.calc_profit(rate=current_rate) <= 0:
return False
logger.debug('Checking sell_signal ...')
if get_signal(trade.pair, SignalType.SELL):
logger.debug('Executing sell due to sell signal ...')
execute_sell(trade, current_rate)
return True
return False
@@ -193,11 +277,12 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def create_trade(stake_amount: float) -> Optional[Trade]:
def create_trade(stake_amount: float) -> 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
:return: True if a trade object has been created and persisted, False otherwise
"""
logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...',
@@ -206,7 +291,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise ValueError(
raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
)
@@ -216,40 +301,51 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist:
raise ValueError('No pair in whitelist')
raise DependencyException('No pair in whitelist')
# Pick pair based on StochRSI buy signals
for _pair in whitelist:
if get_buy_signal(_pair):
if get_signal(_pair, SignalType.BUY):
pair = _pair
break
else:
return None
return False
# Calculate amount and subtract fee
fee = exchange.get_fee()
# Calculate amount
buy_limit = get_target_bid(exchange.get_ticker(pair))
amount = (1 - fee) * stake_amount / buy_limit
amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
fiat_converter = CryptoToFiatConverter()
stake_amount_fiat = fiat_converter.convert_amount(
stake_amount,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
# Create trade entity and return
message = '*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '.format(
exchange.get_name().upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit
)
logger.info(message)
telegram.send_msg(message)
buy_limit, stake_amount, _CONF['stake_currency'],
stake_amount_fiat, _CONF['fiat_display_currency']
))
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
return Trade(pair=pair,
trade = Trade(
pair=pair,
stake_amount=stake_amount,
amount=amount,
fee=fee * 2,
fee=exchange.get_fee(),
open_rate=buy_limit,
open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
open_order_id=order_id)
open_order_id=order_id
)
Trade.session.add(trade)
Trade.session.flush()
return True
def init(config: dict, db_url: Optional[str] = None) -> None:
@@ -260,7 +356,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
:return: None
"""
# Initialize all modules
telegram.init(config)
rpc.init(config)
persistence.init(config, db_url)
exchange.init(config)
@@ -271,17 +367,12 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
else:
update_state(State.STOPPED)
# Register signal handlers
for sig in (SIGINT, SIGTERM, SIGABRT):
signal(sig, cleanup)
@cached(TTLCache(maxsize=1, ttl=1800))
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]:
def gen_pair_whitelist(base_currency: str, key: str = 'BaseVolume') -> List[str]:
"""
Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str
:param topn: maximum number of returned results
:param key: sort key (defaults to 'BaseVolume')
:return: List of pairs
"""
@@ -290,29 +381,36 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum
key=lambda s: s.get(key) or 0.0,
reverse=True
)
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
return [s['MarketName'].replace('-', '_') for s in summaries]
def cleanup(*args, **kwargs) -> None:
def cleanup() -> None:
"""
Cleanup the application state und finish all pending tasks
:return: None
"""
telegram.send_msg('*Status:* `Stopping trader...`')
rpc.send_msg('*Status:* `Stopping trader...`')
logger.info('Stopping trader and cleaning up modules...')
update_state(State.STOPPED)
persistence.cleanup()
telegram.cleanup()
rpc.cleanup()
exit(0)
def main():
def main(sysargv=sys.argv[1:]) -> None:
"""
Loads and validates the config and handles the main loop
:return: None
"""
global _CONF
args = build_arg_parser().parse_args()
args = parse_args(sysargv,
'Simple High Frequency Trading Bot for crypto currencies')
# A subcommand has been issued
if hasattr(args, 'func'):
args.func(args)
exit(0)
# Initialize logger
logging.basicConfig(
@@ -327,23 +425,30 @@ def main():
)
# Load and validate configuration
with open(args.config) as file:
_CONF = json.load(file)
if 'internals' not in _CONF:
_CONF['internals'] = {}
logger.info('Validating configuration ...')
validate(_CONF, CONF_SCHEMA)
_CONF = load_config(args.config)
# Initialize all modules and start main loop
if args.dynamic_whitelist:
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
# If the user ask for Dry run with a local DB instead of memory
if args.dry_run_db:
if _CONF.get('dry_run', False):
_CONF.update({'dry_run_db': True})
logger.info(
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite". (--dry_run_db detected)'
)
else:
logger.info('Dry run is disabled. (--dry_run_db ignored)')
try:
init(_CONF)
old_state = None
while True:
new_state = get_state()
# Log state transition
if new_state != old_state:
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logger.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED:
@@ -352,9 +457,15 @@ def main():
throttle(
_process,
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
dynamic_whitelist=args.dynamic_whitelist,
nb_assets=args.dynamic_whitelist,
)
old_state = new_state
except KeyboardInterrupt:
logger.info('Got SIGINT, aborting ...')
except BaseException:
logger.exception('Got fatal exception!')
finally:
cleanup()
if __name__ == '__main__':

View File

@@ -1,9 +1,13 @@
import argparse
import enum
import json
import logging
from typing import Any, Callable
import time
import os
from typing import Any, Callable, Dict, List
from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match
from wrapt import synchronized
from freqtrade import __version__
@@ -40,6 +44,27 @@ def get_state() -> State:
return _STATE
def load_config(path: str) -> Dict:
"""
Loads a config file from the given path
:param path: path as str
:return: configuration as dictionary
"""
with open(path) as file:
conf = json.load(file)
if 'internals' not in conf:
conf['internals'] = {}
logger.info('Validating configuration ...')
try:
validate(conf, CONF_SCHEMA)
return conf
except ValidationError as exception:
logger.fatal('Invalid configuration. See config.json.example. Reason: %s', exception)
raise ValidationError(
best_match(Draft4Validator(CONF_SCHEMA).iter_errors(conf)).message
)
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
"""
Throttles the given callable that it
@@ -57,18 +82,12 @@ def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
return result
def build_arg_parser() -> argparse.ArgumentParser:
""" Builds and returns an ArgumentParser instance """
def common_args_parser(description: str):
"""
Parses given common arguments and returns them as a parsed object.
"""
parser = argparse.ArgumentParser(
description='Simple High Frequency Trading Bot for crypto currencies'
)
parser.add_argument(
'-c', '--config',
help='specify configuration file (default: config.json)',
dest='config',
default='config.json',
type=str,
metavar='PATH',
description=description
)
parser.add_argument(
'-v', '--verbose',
@@ -84,13 +103,116 @@ def build_arg_parser() -> argparse.ArgumentParser:
version='%(prog)s {}'.format(__version__),
)
parser.add_argument(
'--dynamic-whitelist',
help='dynamically generate and update whitelist based on 24h BaseVolume',
action='store_true',
'-c', '--config',
help='specify configuration file (default: config.json)',
dest='config',
default='config.json',
type=str,
metavar='PATH',
)
return parser
def parse_args(args: List[str], description: str):
"""
Parses given arguments and returns an argparse Namespace instance.
Returns None if a sub command has been selected and executed.
"""
parser = common_args_parser(description)
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',
)
parser.add_argument(
'-dd', '--datadir',
help='path to backtest data (default freqdata/tests/testdata',
dest='datadir',
default=os.path.join('freqtrade', 'tests', 'testdata'),
type=str,
metavar='PATH',
)
parser.add_argument(
'--dynamic-whitelist',
help='dynamically generate and update whitelist \
based on 24h BaseVolume (Default 20 currencies)', # noqa
dest='dynamic_whitelist',
const=20,
type=int,
metavar='INT',
nargs='?',
)
build_subcommands(parser)
return parser.parse_args(args)
def build_subcommands(parser: argparse.ArgumentParser) -> None:
""" Builds and attaches all subcommands """
from freqtrade.optimize import backtesting, hyperopt
subparsers = parser.add_subparsers(dest='subparser')
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
backtesting_cmd.set_defaults(func=backtesting.start)
backtesting_cmd.add_argument(
'-l', '--live',
action='store_true',
dest='live',
help='using live data',
)
backtesting_cmd.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval in minutes (default: 5)',
dest='ticker_interval',
default=5,
type=int,
metavar='INT',
)
backtesting_cmd.add_argument(
'--realistic-simulation',
help='uses max_open_trades from config to simulate real world limitations',
action='store_true',
dest='realistic_simulation',
)
backtesting_cmd.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.',
action='store_true',
dest='refresh_pairs',
)
# Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
hyperopt_cmd.set_defaults(func=hyperopt.start)
hyperopt_cmd.add_argument(
'-e', '--epochs',
help='specify number of epochs (default: 100)',
dest='epochs',
default=100,
type=int,
metavar='INT',
)
hyperopt_cmd.add_argument(
'--use-mongodb',
help='parallelize evaluations with mongodb (requires mongod in PATH)',
dest='mongodb',
action='store_true',
)
hyperopt_cmd.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval in minutes (default: 5)',
dest='ticker_interval',
default=5,
type=int,
metavar='INT',
)
# Required json-schema for user specified config
CONF_SCHEMA = {
'type': 'object',
@@ -98,6 +220,14 @@ CONF_SCHEMA = {
'max_open_trades': {'type': 'integer', 'minimum': 1},
'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']},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
@@ -107,6 +237,7 @@ CONF_SCHEMA = {
'minProperties': 1
},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
'bid_strategy': {
'type': 'object',
'properties': {
@@ -120,6 +251,13 @@ CONF_SCHEMA = {
'required': ['ask_last_balance']
},
'exchange': {'$ref': '#/definitions/exchange'},
'experimental': {
'type': 'object',
'properties': {
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}
}
},
'telegram': {
'type': 'object',
'properties': {
@@ -146,7 +284,18 @@ CONF_SCHEMA = {
'secret': {'type': 'string'},
'pair_whitelist': {
'type': 'array',
'items': {'type': 'string'},
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
},
'pair_blacklist': {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
},
'uniqueItems': True
}
},
@@ -160,6 +309,7 @@ CONF_SCHEMA = {
'max_open_trades',
'stake_currency',
'stake_amount',
'fiat_display_currency',
'dry_run',
'minimal_roi',
'bid_strategy',

View File

@@ -0,0 +1,133 @@
# pragma pylint: disable=missing-docstring
import logging
import json
import os
from typing import Optional, List, Dict
from pandas import DataFrame
from freqtrade.exchange import get_ticker_history
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
logger = logging.getLogger(__name__)
def load_tickerdata_file(datadir, pair, ticker_interval):
"""
Load a pair from file,
:return dict OR empty if unsuccesful
"""
path = make_testdata_path(datadir)
file = '{abspath}/{pair}-{ticker_interval}.json'.format(
abspath=path,
pair=pair,
ticker_interval=ticker_interval,
)
# The file does not exist we download it
if not os.path.isfile(file):
return None
# Read the file, load the json
with open(file) as tickerdata:
pairdata = json.load(tickerdata)
return pairdata
def load_data(datadir: str, ticker_interval: int = 5, pairs: Optional[List[str]] = None,
refresh_pairs: Optional[bool] = False) -> Dict[str, List]:
"""
Loads ticker history data for the given parameters
:param ticker_interval: ticker interval in minutes
:param pairs: list of pairs
: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)
for pair in _pairs:
pairdata = load_tickerdata_file(datadir, pair, ticker_interval)
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)
result[pair] = pairdata
return result
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""Creates a dataframe and populates indicators for given ticker data"""
return {pair: populate_indicators(parse_ticker_dataframe(pair_data))
for pair, pair_data in tickerdata.items()}
def make_testdata_path(datadir: str) -> str:
"""Return the path where testdata files are stored"""
return datadir or os.path.abspath(os.path.join(os.path.dirname(__file__),
'..', 'tests', 'testdata'))
def download_pairs(datadir, pairs: List[str]) -> bool:
"""For each pairs passed in parameters, download 1 and 5 ticker intervals"""
for pair in pairs:
try:
for interval in [1, 5]:
download_backtesting_testdata(datadir, pair=pair, interval=interval)
except BaseException:
logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format(
pair=pair,
interval=interval,
))
return False
return True
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> bool:
"""
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
:param pairs: list of pairs to download
:return: bool
"""
path = make_testdata_path(datadir)
logger.info('Download the pair: "{pair}", Interval: {interval} min'.format(
pair=pair,
interval=interval,
))
filepair = pair.replace("-", "_")
filename = os.path.join(path, '{pair}-{interval}.json'.format(
pair=filepair,
interval=interval,
))
filename = filename.replace('USDT_BTC', 'BTC_FAKEBULL')
if os.path.isfile(filename):
with open(filename, "rt") as fp:
data = json.load(fp)
logger.debug("Current Start: {}".format(data[1]['T']))
logger.debug("Current End: {}".format(data[-1:][0]['T']))
else:
data = []
logger.debug("Current Start: None")
logger.debug("Current End: None")
new_data = get_ticker_history(pair=pair, tick_interval=int(interval))
for row in new_data:
if row not in data:
data.append(row)
logger.debug("New Start: {}".format(data[1]['T']))
logger.debug("New End: {}".format(data[-1:][0]['T']))
data = sorted(data, key=lambda data: data['T'])
with open(filename, "wt") as fp:
json.dump(data, fp)
return True

View File

@@ -0,0 +1,197 @@
# pragma pylint: disable=missing-docstring,W0212
import logging
from typing import Dict, Tuple
import arrow
from pandas import DataFrame, Series
from tabulate import tabulate
import freqtrade.misc as misc
import freqtrade.optimize as optimize
from freqtrade import exchange
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
from freqtrade.exchange import Bittrex
from freqtrade.main import min_roi_reached
from freqtrade.optimize import preprocess
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
"""
Get the maximum timeframe for the given backtest data
:param data: dictionary with preprocessed backtesting data
:return: tuple containing min_date, max_date
"""
all_dates = Series([])
for pair, pair_data in data.items():
all_dates = all_dates.append(pair_data['date'])
all_dates.sort_values(inplace=True)
return arrow.get(all_dates.iloc[0]), arrow.get(all_dates.iloc[-1])
def generate_text_table(
data: Dict[str, Dict], results: DataFrame, stake_currency, ticker_interval) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
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]
tabular_data.append([
pair,
len(result.index),
result.profit_percent.mean() * 100.0,
result.profit_BTC.sum(),
result.duration.mean() * ticker_interval,
result.profit.sum(),
result.loss.sum()
])
# Append Total
tabular_data.append([
'TOTAL',
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(),
results.duration.mean() * ticker_interval,
results.profit.sum(),
results.loss.sum()
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
def backtest(stake_amount: float, processed: Dict[str, DataFrame],
max_open_trades: int = 0, realistic: bool = True, sell_profit_only: bool = False,
stoploss: int = -1.00, use_sell_signal: bool = False) -> DataFrame:
"""
Implements backtesting functionality
:param stake_amount: btc amount to use for each trade
:param processed: a processed dictionary with format {pair, data}
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
:param realistic: do we try to simulate realistic trades? (default: True)
:return: DataFrame
"""
trades = []
trade_count_lock: dict = {}
exchange._API = Bittrex({'key': '', 'secret': ''})
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0
ticker = populate_sell_trend(populate_buy_trend(pair_data))
# for each buy point
lock_pair_until = None
buy_subset = ticker[ticker.buy == 1][['buy', 'open', 'close', 'date', 'sell']]
for row in buy_subset.itertuples(index=True):
if realistic:
if lock_pair_until is not None and row.Index <= lock_pair_until:
continue
if max_open_trades > 0:
# Check if max_open_trades has already been reached for the given date
if not trade_count_lock.get(row.date, 0) < max_open_trades:
continue
if max_open_trades > 0:
# Increase lock
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
trade = Trade(
open_rate=row.close,
open_date=row.date,
stake_amount=stake_amount,
amount=stake_amount / row.open,
fee=exchange.get_fee()
)
# calculate win/lose forwards from buy point
sell_subset = ticker[row.Index + 1:][['close', 'date', 'sell']]
for row2 in sell_subset.itertuples(index=True):
if max_open_trades > 0:
# Increase trade_count_lock for every iteration
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
if (sell_profit_only and current_profit_percent < 0):
continue
if min_roi_reached(trade, row2.close, row2.date) or \
(row2.sell == 1 and use_sell_signal) or \
current_profit_percent <= stoploss:
current_profit_btc = trade.calc_profit(rate=row2.close)
lock_pair_until = row2.Index
trades.append(
(
pair,
current_profit_percent,
current_profit_btc,
row2.Index - row.Index,
current_profit_btc > 0,
current_profit_btc < 0
)
)
break
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'profit', 'loss']
return DataFrame.from_records(trades, columns=labels)
def start(args):
# Initialize logger
logging.basicConfig(
level=args.loglevel,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
exchange._API = Bittrex({'key': '', 'secret': ''})
logger.info('Using config: %s ...', args.config)
config = misc.load_config(args.config)
logger.info('Using ticker_interval: %s ...', args.ticker_interval)
data = {}
pairs = config['exchange']['pair_whitelist']
if args.live:
logger.info('Downloading data for all pairs in whitelist ...')
for pair in pairs:
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
data = optimize.load_data(args.datadir, pairs=pairs, ticker_interval=args.ticker_interval,
refresh_pairs=args.refresh_pairs)
logger.info('Using stake_currency: %s ...', config['stake_currency'])
logger.info('Using stake_amount: %s ...', config['stake_amount'])
max_open_trades = 0
if args.realistic_simulation:
logger.info('Using max_open_trades: %s ...', config['max_open_trades'])
max_open_trades = config['max_open_trades']
# Monkey patch config
from freqtrade import main
main._CONF = config
preprocessed = preprocess(data)
# Print timeframe
min_date, max_date = get_timeframe(preprocessed)
logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat())
# Execute backtest and print results
results = backtest(
stake_amount=config['stake_amount'],
processed=preprocessed,
max_open_trades=max_open_trades,
realistic=args.realistic_simulation,
sell_profit_only=config.get('experimental', {}).get('sell_profit_only', False),
stoploss=config.get('stoploss'),
use_sell_signal=config.get('experimental', {}).get('use_sell_signal', False)
)
logger.info(
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
generate_text_table(data, results, config['stake_currency'], args.ticker_interval)
)

View File

@@ -0,0 +1,318 @@
# pragma pylint: disable=missing-docstring,W0212,W0603
import json
import logging
import sys
import pickle
import signal
import os
from functools import reduce
from math import exp
from operator import itemgetter
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
from hyperopt.mongoexp import MongoTrials
from pandas import DataFrame
from freqtrade import main # noqa
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.misc import load_config
from freqtrade.optimize.backtesting import backtest
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
from freqtrade.vendor.qtpylib.indicators import crossed_above
# Remove noisy log messages
logging.getLogger('hyperopt.mongoexp').setLevel(logging.WARNING)
logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
logger = logging.getLogger(__name__)
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
TARGET_TRADES = 1100
TOTAL_TRIES = 0
_CURRENT_TRIES = 0
CURRENT_BEST_LOSS = 100
# max average trade duration in minutes
# if eval ends with higher value, we consider it a failed eval
MAX_ACCEPTED_TRADE_DURATION = 240
# this is expexted avg profit * expected trade count
# for example 3.5%, 1100 trades, EXPECTED_MAX_PROFIT = 3.85
EXPECTED_MAX_PROFIT = 3.85
# Configuration and data used by hyperopt
PROCESSED = None # optimize.preprocess(optimize.load_data())
OPTIMIZE_CONFIG = hyperopt_optimize_conf()
# Hyperopt Trials
TRIALS_FILE = os.path.join('freqtrade', 'optimize', 'hyperopt_trials.pickle')
TRIALS = Trials()
# Monkey patch config
from freqtrade import main # noqa
main._CONF = OPTIMIZE_CONFIG
SPACE = {
'mfi': hp.choice('mfi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
]),
'fastd': hp.choice('fastd', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
{'enabled': False},
{'enabled': True}
]),
'over_sar': hp.choice('over_sar', [
{'enabled': False},
{'enabled': True}
]),
'green_candle': hp.choice('green_candle', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_sma': hp.choice('uptrend_sma', [
{'enabled': False},
{'enabled': True}
]),
'trigger': hp.choice('trigger', [
{'type': 'lower_bb'},
{'type': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]),
'stoploss': hp.uniform('stoploss', -0.5, -0.02),
}
def save_trials(trials, trials_path=TRIALS_FILE):
"""Save hyperopt trials to file"""
logger.info('Saving Trials to \'{}\''.format(trials_path))
pickle.dump(trials, open(trials_path, 'wb'))
def read_trials(trials_path=TRIALS_FILE):
"""Read hyperopt trials file"""
logger.info('Reading Trials from \'{}\''.format(trials_path))
trials = pickle.load(open(trials_path, 'rb'))
os.remove(trials_path)
return trials
def log_trials_result(trials):
vals = json.dumps(trials.best_trial['misc']['vals'], indent=4)
results = trials.best_trial['result']['result']
logger.info('Best result:\n%s\nwith values:\n%s', results, vals)
def log_results(results):
""" log results if it is better than any previous evaluation """
global CURRENT_BEST_LOSS
if results['loss'] < CURRENT_BEST_LOSS:
CURRENT_BEST_LOSS = results['loss']
logger.info('{:5d}/{}: {}'.format(
results['current_tries'],
results['total_tries'],
results['result']))
else:
print('.', end='')
sys.stdout.flush()
def calculate_loss(total_profit: float, trade_count: int, trade_duration: float):
""" objective function, returns smaller number for more optimal results """
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
duration_loss = min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1)
return trade_loss + profit_loss + duration_loss
def optimizer(params):
global _CURRENT_TRIES
from freqtrade.optimize import backtesting
backtesting.populate_buy_trend = buy_strategy_generator(params)
results = backtest(OPTIMIZE_CONFIG['stake_amount'], PROCESSED, stoploss=params['stoploss'])
result_explanation = format_results(results)
total_profit = results.profit_percent.sum()
trade_count = len(results.index)
trade_duration = results.duration.mean() * 5
if trade_count == 0 or trade_duration > MAX_ACCEPTED_TRADE_DURATION:
print('.', end='')
return {
'status': STATUS_FAIL,
'loss': float('inf')
}
loss = calculate_loss(total_profit, trade_count, trade_duration)
_CURRENT_TRIES += 1
log_results({
'loss': loss,
'current_tries': _CURRENT_TRIES,
'total_tries': TOTAL_TRIES,
'result': result_explanation,
})
return {
'loss': loss,
'status': STATUS_OK,
'result': result_explanation,
}
def format_results(results: DataFrame):
return ('{:6d} trades. Avg profit {: 5.2f}%. '
'Total profit {: 11.8f} BTC. Avg duration {:5.1f} mins.').format(
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(),
results.duration.mean() * 5,
)
def buy_strategy_generator(params):
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if params['uptrend_long_ema']['enabled']:
conditions.append(dataframe['ema50'] > dataframe['ema100'])
if params['uptrend_short_ema']['enabled']:
conditions.append(dataframe['ema5'] > dataframe['ema10'])
if params['mfi']['enabled']:
conditions.append(dataframe['mfi'] < params['mfi']['value'])
if params['fastd']['enabled']:
conditions.append(dataframe['fastd'] < params['fastd']['value'])
if params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
if params['over_sar']['enabled']:
conditions.append(dataframe['close'] > dataframe['sar'])
if params['green_candle']['enabled']:
conditions.append(dataframe['close'] > dataframe['open'])
if params['uptrend_sma']['enabled']:
prevsma = dataframe['sma'].shift(1)
conditions.append(dataframe['sma'] > prevsma)
# TRIGGERS
triggers = {
'lower_bb': dataframe['tema'] <= dataframe['blower'],
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
}
conditions.append(triggers.get(params['trigger']['type']))
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
return dataframe
return populate_buy_trend
def start(args):
global TOTAL_TRIES, PROCESSED, SPACE, TRIALS, _CURRENT_TRIES
TOTAL_TRIES = args.epochs
exchange._API = Bittrex({'key': '', 'secret': ''})
# Initialize logger
logging.basicConfig(
level=args.loglevel,
format='\n%(message)s',
)
logger.info('Using config: %s ...', args.config)
config = load_config(args.config)
pairs = config['exchange']['pair_whitelist']
PROCESSED = optimize.preprocess(optimize.load_data(
args.datadir, pairs=pairs, ticker_interval=args.ticker_interval))
if args.mongodb:
logger.info('Using mongodb ...')
logger.info('Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!')
db_name = 'freqtrade_hyperopt'
TRIALS = MongoTrials('mongo://127.0.0.1:1234/{}/jobs'.format(db_name), exp_key='exp1')
else:
logger.info('Preparing Trials..')
signal.signal(signal.SIGINT, signal_handler)
# read trials file if we have one
if os.path.exists(TRIALS_FILE):
TRIALS = read_trials()
_CURRENT_TRIES = len(TRIALS.results)
TOTAL_TRIES = TOTAL_TRIES + _CURRENT_TRIES
logger.info(
'Continuing with trials. Current: {}, Total: {}'
.format(_CURRENT_TRIES, TOTAL_TRIES))
try:
best_parameters = fmin(
fn=optimizer,
space=SPACE,
algo=tpe.suggest,
max_evals=TOTAL_TRIES,
trials=TRIALS
)
results = sorted(TRIALS.results, key=itemgetter('loss'))
best_result = results[0]['result']
except ValueError:
best_parameters = {}
best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \
'try with more epochs (param: -e).'
# Improve best parameter logging display
if best_parameters:
best_parameters = space_eval(SPACE, best_parameters)
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
logger.info('Best Result:\n%s', best_result)
# Store trials result to file to resume next time
save_trials(TRIALS)
def signal_handler(sig, frame):
"""Hyperopt SIGINT handler"""
logger.info('Hyperopt received {}'.format(signal.Signals(sig).name))
save_trials(TRIALS)
log_trials_result(TRIALS)
sys.exit(0)

View File

@@ -0,0 +1,41 @@
"""
File that contains the configuration for Hyperopt
"""
def hyperopt_optimize_conf() -> dict:
"""
This function is used to define which parameters Hyperopt must used.
The "pair_whitelist" is only used is your are using Hyperopt with MongoDB,
without MongoDB, Hyperopt will use the pair your have set in your config file.
:return:
"""
return {
'max_open_trades': 3,
'stake_currency': 'BTC',
'stake_amount': 0.01,
"minimal_roi": {
'40': 0.0,
'30': 0.01,
'20': 0.02,
'0': 0.04,
},
'stoploss': -0.10,
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"pair_whitelist": [
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
]
}
}

View File

@@ -1,10 +1,11 @@
import logging
from datetime import datetime
from decimal import Decimal, getcontext
from typing import Optional, Dict
from typing import Dict, Optional
import arrow
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
create_engine)
from sqlalchemy.engine import Engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session
@@ -29,6 +30,11 @@ def init(config: dict, engine: Optional[Engine] = None) -> 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,
@@ -68,7 +74,7 @@ class Trade(_DECL_BASE):
open_order_id = Column(String)
def __repr__(self):
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
self.id,
self.pair,
self.amount,
@@ -82,25 +88,99 @@ class Trade(_DECL_BASE):
:param order: order retrieved by exchange.get_order()
:return: None
"""
if not order['closed']:
# Ignore open and cancelled orders
if not order['closed'] or order['rate'] is None:
return
logger.debug('Updating trade (id=%d) ...', self.id)
logger.info('Updating trade (id=%d) ...', self.id)
getcontext().prec = 8 # Bittrex do not go above 8 decimal
if order['type'] == 'LIMIT_BUY':
# Update open rate and actual amount
self.open_rate = order['rate']
self.amount = order['amount']
self.open_rate = Decimal(order['rate'])
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':
# Set close rate and set actual profit
self.close_rate = order['rate']
self.close_profit = self.calc_profit()
self.close_date = datetime.utcnow()
self.close(order['rate'])
else:
raise ValueError('Unknown order type: {}'.format(order['type']))
cleanup()
def close(self, rate: float) -> None:
"""
Sets close_rate to the given rate, calculates total profit
and marks trade as closed
"""
self.close_rate = Decimal(rate)
self.close_profit = self.calc_profit_percent()
self.close_date = datetime.utcnow()
self.is_open = False
self.open_order_id = None
logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
self
)
def calc_profit(self, rate: Optional[float] = None) -> float:
def calc_open_trade_price(
self,
fee: Optional[float] = None) -> float:
"""
Calculate the open_rate in BTC
:param fee: fee to use on the open rate (optional).
If rate is not set self.fee will be used
:return: Price in BTC of the open trade
"""
getcontext().prec = 8
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
fees = buy_trade * Decimal(fee or self.fee)
return float(buy_trade + fees)
def calc_close_trade_price(
self,
rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
"""
Calculate the close_rate in BTC
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:return: Price in BTC of the open trade
"""
getcontext().prec = 8
if rate is None and not self.close_rate:
return 0.0
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
fees = sell_trade * Decimal(fee or self.fee)
return float(sell_trade - fees)
def calc_profit(
self,
rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
"""
Calculate the profit in BTC between Close and Open trade
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used
:return: profit in BTC as float
"""
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=Decimal(rate or self.close_rate),
fee=Decimal(fee or self.fee)
)
return float("{0:.8f}".format(close_trade_price - open_trade_price))
def calc_profit_percent(
self,
rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
"""
Calculates the profit in percentage (including fee).
:param rate: rate to compare with (optional).
@@ -108,5 +188,11 @@ class Trade(_DECL_BASE):
:return: profit in percentage as float
"""
getcontext().prec = 8
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
/ Decimal(self.open_rate) - Decimal(self.fee))
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=Decimal(rate or self.close_rate),
fee=Decimal(fee or self.fee)
)
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))

View File

@@ -1 +1,42 @@
import logging
from . import telegram
logger = logging.getLogger(__name__)
REGISTERED_MODULES = []
def init(config: dict) -> None:
"""
Initializes all enabled rpc modules
:param config: config to use
:return: None
"""
if config['telegram'].get('enabled', False):
logger.info('Enabling rpc.telegram ...')
REGISTERED_MODULES.append('telegram')
telegram.init(config)
def cleanup() -> None:
"""
Stops all enabled rpc modules
:return: None
"""
if 'telegram' in REGISTERED_MODULES:
logger.debug('Cleaning up rpc.telegram ...')
telegram.cleanup()
def send_msg(msg: str) -> None:
"""
Send given markdown message to all registered rpc modules
:param msg: message
:return: None
"""
logger.info(msg)
if 'telegram' in REGISTERED_MODULES:
telegram.send_msg(msg)

View File

@@ -1,18 +1,20 @@
import logging
import re
from datetime import timedelta
from typing import Callable, Any
from pandas import DataFrame
from tabulate import tabulate
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Any, Callable
import arrow
from pandas import DataFrame
from sqlalchemy import and_, func, text
from telegram import ParseMode, Bot, Update
from telegram.error import NetworkError
from tabulate import tabulate
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater
from freqtrade import exchange, __version__
from freqtrade.misc import get_state, State, update_state
from freqtrade import __version__, exchange
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.misc import State, get_state, update_state
from freqtrade.persistence import Trade
# Remove noisy log messages
@@ -22,6 +24,7 @@ logger = logging.getLogger(__name__)
_UPDATER: Updater = None
_CONF = {}
_FIAT_CONVERT = CryptoToFiatConverter()
def init(config: dict) -> None:
@@ -49,6 +52,7 @@ def init(config: dict) -> None:
CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance),
CommandHandler('daily', _daily),
CommandHandler('count', _count),
CommandHandler('help', _help),
CommandHandler('version', _version),
@@ -57,7 +61,7 @@ def init(config: dict) -> None:
_UPDATER.dispatcher.add_handler(handle)
_UPDATER.start_polling(
clean=True,
bootstrap_retries=3,
bootstrap_retries=-1,
timeout=30,
read_latency=60,
)
@@ -91,7 +95,7 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
:return: decorated function
"""
def wrapper(*args, **kwargs):
bot, update = kwargs.get('bot') or args[0], kwargs.get('update') or args[1]
update = kwargs.get('update') or args[1]
# Reject unauthorized messages
chat_id = int(_CONF['telegram']['chat_id'])
@@ -136,8 +140,8 @@ def _status(bot: Bot, update: Update) -> None:
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair)['bid']
current_profit = trade.calc_profit(current_rate)
current_rate = 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)
) if trade.close_profit else None
@@ -163,8 +167,8 @@ def _status(bot: Bot, update: Update) -> None:
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit * 100, 2),
open_order='{} ({})'.format(
order['remaining'], order['type']
open_order='({} rem={:.8f})'.format(
order['type'], order['remaining']
) if order else None,
)
send_msg(message, bot=bot)
@@ -189,12 +193,12 @@ def _status_table(bot: Bot, update: Update) -> None:
trades_list = []
for trade in trades:
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair)['bid']
current_rate = exchange.get_ticker(trade.pair, False)['bid']
trades_list.append([
trade.id,
trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}'.format(100 * trade.calc_profit(current_rate))
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
])
columns = ['ID', 'Pair', 'Since', 'Profit']
@@ -207,6 +211,65 @@ def _status_table(bot: Bot, update: Update) -> None:
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _daily(bot: Bot, update: Update) -> None:
"""
Handler for /daily <n>
Returns a daily profit (in BTC) over the last n days.
:param bot: telegram bot
:param update: message update
:return: None
"""
today = datetime.utcnow().date()
profit_days = {}
try:
timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError):
timescale = 7
if not (isinstance(timescale, int) and timescale > 0):
send_msg('*Daily [n]:* `must be an integer greater than 0`', bot=bot)
return
for day in range(0, timescale):
profitday = today - timedelta(days=day)
trades = Trade.query \
.filter(Trade.is_open.is_(False)) \
.filter(Trade.close_date >= profitday)\
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
.order_by(Trade.close_date)\
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = format(curdayprofit, '.8f')
stats = [
[
key,
'{value:.8f} {symbol}'.format(value=float(value), symbol=_CONF['stake_currency']),
'{value:.3f} {symbol}'.format(
value=_FIAT_CONVERT.convert_amount(
value,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
),
symbol=_CONF['fiat_display_currency']
)
]
for key, value in profit_days.items()
]
stats = tabulate(stats,
headers=[
'Day',
'Profit {}'.format(_CONF['stake_currency']),
'Profit {}'.format(_CONF['fiat_display_currency'])
],
tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(timescale, stats)
send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@authorized_only
def _profit(bot: Bot, update: Update) -> None:
"""
@@ -218,23 +281,31 @@ def _profit(bot: Bot, update: Update) -> None:
"""
trades = Trade.query.order_by(Trade.id).all()
profit_amounts = []
profits = []
profit_all_coin = []
profit_all_percent = []
profit_closed_coin = []
profit_closed_percent = []
durations = []
for trade in trades:
current_rate = None
if not trade.open_rate:
continue
if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds())
if trade.close_profit:
profit = trade.close_profit
if not trade.is_open:
profit_percent = trade.calc_profit_percent()
profit_closed_coin.append(trade.calc_profit())
profit_closed_percent.append(profit_percent)
else:
# Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid']
profit = trade.calc_profit(current_rate)
current_rate = exchange.get_ticker(trade.pair, False)['bid']
profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_amounts.append(profit * trade.stake_amount)
profits.append(profit)
profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
profit_all_percent.append(profit_percent)
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
.filter(Trade.is_open.is_(False)) \
@@ -247,16 +318,46 @@ def _profit(bot: Bot, update: Update) -> None:
return
bp_pair, bp_rate = best_pair
# 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_fiat = _FIAT_CONVERT.convert_amount(
profit_closed_coin,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
profit_all_fiat = _FIAT_CONVERT.convert_amount(
profit_all_coin,
_CONF['stake_currency'],
_CONF['fiat_display_currency']
)
# Message to display
markdown_msg = """
*ROI:* `{profit_btc:.8f} ({profit:.2f}%)`
*Trade Count:* `{trade_count}`
*ROI:* Close trades
∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`
∙ `{profit_closed_fiat:.3f} {fiat}`
*ROI:* All trades
∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`
∙ `{profit_all_fiat:.3f} {fiat}`
*Total Trade Count:* `{trade_count}`
*First Trade opened:* `{first_trade_date}`
*Latest Trade opened:* `{latest_trade_date}`
*Avg. Duration:* `{avg_duration}`
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
""".format(
profit_btc=round(sum(profit_amounts), 8),
profit=round(sum(profits) * 100, 2),
coin=_CONF['stake_currency'],
fiat=_CONF['fiat_display_currency'],
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(),
@@ -339,10 +440,7 @@ def _forcesell(bot: Bot, update: Update) -> None:
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
# Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid']
from freqtrade.main import execute_sell
execute_sell(trade, current_rate)
_exec_forcesell(trade)
return
# Query for trade
@@ -354,10 +452,8 @@ def _forcesell(bot: Bot, update: Update) -> None:
send_msg('Invalid argument. See `/help` to view usage')
logger.warning('/forcesell: Invalid argument received')
return
# Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid']
from freqtrade.main import execute_sell
execute_sell(trade, current_rate)
_exec_forcesell(trade)
@authorized_only
@@ -373,17 +469,19 @@ def _performance(bot: Bot, update: Update) -> None:
send_msg('`trader is not running`', bot=bot)
return
pair_rates = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
pair_rates = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum'),
func.count(Trade.pair).label('count')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(text('profit_sum DESC')) \
.all()
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}%</code>'.format(
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
index=i + 1,
pair=pair,
profit=round(rate * 100, 2)
) for i, (pair, rate) in enumerate(pair_rates))
profit=round(rate * 100, 2),
count=count
) for i, (pair, rate, count) in enumerate(pair_rates))
message = '<b>Performance:</b>\n{}'.format(stats)
logger.debug(message)
@@ -431,6 +529,7 @@ def _help(bot: Bot, update: Update) -> None:
*/profit:* `Lists cumulative profit from all finished trades`
*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, regardless of profit`
*/performance:* `Show performance of each finished trade grouped by pair`
*/daily <n>:* `Shows profit or loss per day, over the last n days`
*/count:* `Show number of trades running compared to allowed number of trades`
*/balance:* `Show account balance per currency`
*/help:* `This help message`
@@ -451,11 +550,11 @@ def _version(bot: Bot, update: Update) -> None:
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def shorten_date(date):
def shorten_date(_date):
"""
Trim the date so it fits on small screens
"""
new_date = re.sub('seconds?', 'sec', date)
new_date = re.sub('seconds?', 'sec', _date)
new_date = re.sub('minutes?', 'min', new_date)
new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
@@ -463,6 +562,28 @@ def shorten_date(date):
return new_date
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)
# 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
# Ignore trades with an attached LIMIT_SELL order
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
return
# Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid']
from freqtrade.main import execute_sell
execute_sell(trade, current_rate)
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message
@@ -475,13 +596,29 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
return
bot = bot or _UPDATER.bot
keyboard = [['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help']]
reply_markup = ReplyKeyboardMarkup(keyboard)
try:
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
except NetworkError as error:
try:
bot.send_message(
_CONF['telegram']['chat_id'], msg,
parse_mode=parse_mode, reply_markup=reply_markup
)
except NetworkError as network_err:
# Sometimes the telegram server resets the current connection,
# if this is the case we send the message again.
logger.warning(
'Got Telegram NetworkError: %s! Trying one more time.',
error.message
network_err.message
)
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
bot.send_message(
_CONF['telegram']['chat_id'], msg,
parse_mode=parse_mode, reply_markup=reply_markup
)
except TelegramError as telegram_err:
logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message)

View File

@@ -1,11 +1,11 @@
# pragma pylint: disable=missing-docstring
import json
from datetime import datetime
from unittest.mock import MagicMock
import arrow
import pytest
from jsonschema import validate
from telegram import Message, Chat, Update
from telegram import Chat, Message, Update
from freqtrade.misc import CONF_SCHEMA
@@ -16,7 +16,8 @@ def default_conf():
configuration = {
"max_open_trades": 1,
"stake_currency": "BTC",
"stake_amount": 0.05,
"stake_amount": 0.001,
"fiat_display_currency": "USD",
"dry_run": True,
"minimal_roi": {
"40": 0.0,
@@ -24,7 +25,8 @@ def default_conf():
"20": 0.02,
"0": 0.04
},
"stoploss": -0.05,
"stoploss": -0.10,
"unfilledtimeout": 600,
"bid_strategy": {
"ask_last_balance": 0.0
},
@@ -52,29 +54,6 @@ def default_conf():
return configuration
@pytest.fixture(scope="module")
def backtest_conf():
return {
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.05
}
@pytest.fixture(scope="module")
def backdata():
result = {}
for pair in ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']:
with open('freqtrade/tests/testdata/' + pair + '.json') as data_file:
result[pair] = json.load(data_file)
return result
@pytest.fixture
def update():
_update = Update(0)
@@ -85,9 +64,27 @@ def update():
@pytest.fixture
def ticker():
return MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061,
'bid': 0.00001098,
'ask': 0.00001099,
'last': 0.00001098,
})
@pytest.fixture
def ticker_sell_up():
return MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172,
})
@pytest.fixture
def ticker_sell_down():
return MagicMock(return_value={
'bid': 0.00001044,
'ask': 0.00001043,
'last': 0.00001044,
})
@@ -127,11 +124,50 @@ def limit_buy_order():
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.07256061,
'amount': 206.43811673387373,
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001099,
'amount': 90.99181073,
'remaining': 0.0,
'closed': datetime.utcnow(),
'closed': str(arrow.utcnow().datetime),
}
@pytest.fixture
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,
'amount': 90.99181073,
'remaining': 90.99181073,
}
@pytest.fixture
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,
'amount': 90.99181073,
'remaining': 90.99181073,
}
@pytest.fixture
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,
'amount': 90.99181073,
'remaining': 67.99181073,
}
@@ -141,9 +177,42 @@ def limit_sell_order():
'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.0802134,
'amount': 206.43811673387373,
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001173,
'amount': 90.99181073,
'remaining': 0.0,
'closed': datetime.utcnow(),
'closed': str(arrow.utcnow().datetime),
}
@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
}
]

View File

@@ -0,0 +1,211 @@
# pragma pylint: disable=missing-docstring,C0103
from unittest.mock import MagicMock
from requests.exceptions import RequestException
from random import randint
import logging
import pytest
from freqtrade import OperationalException
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
get_ticker, cancel_order, get_name, get_fee
def test_init(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
init(config=default_conf)
assert ('freqtrade.exchange',
logging.INFO,
'Instance is running with dry_run enabled'
) in caplog.record_tuples
def test_init_exception(default_conf, mocker):
default_conf['exchange']['name'] = 'wrong_exchange_name'
with pytest.raises(
OperationalException,
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
init(config=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'])
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)
with pytest.raises(OperationalException, match=r'not available'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
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)
with pytest.raises(OperationalException, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
def test_validate_pairs_exception(default_conf, mocker, caplog):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(side_effect=RequestException())
mocker.patch('freqtrade.exchange._API', api_mock)
# with pytest.raises(RequestException, match=r'Unable to validate pairs'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
assert ('freqtrade.exchange',
logging.WARNING,
'Unable to validate pairs (assuming they are correct). Reason: '
) in caplog.record_tuples
def test_buy_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
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)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', 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)
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
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)
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)
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
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)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balance(currency='BTC') == 123.4
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() == []
def test_get_balances_prod(default_conf, mocker):
balance_item = {
'Currency': '1ST',
'Balance': 10.0,
'Available': 10.0,
'Pending': 0.0,
'CryptoAddress': None
}
api_mock = MagicMock()
api_mock.get_balances = MagicMock(
return_value=[balance_item, balance_item, balance_item])
mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
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
def test_get_ticker(mocker, ticker):
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)
# retrieve original ticker
ticker = get_ticker(pair='BTC_ETH')
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)
# if not caching the result we should get the same ticker
ticker = get_ticker(pair='BTC_ETH', refresh=False)
assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099
# force ticker refresh
ticker = get_ticker(pair='BTC_ETH', refresh=True)
assert ticker['bid'] == 0.5
assert ticker['ask'] == 1
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
def test_get_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'bittrex'
init(default_conf)
assert get_name() == 'Bittrex'
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

View File

@@ -0,0 +1,337 @@
# pragma pylint: disable=missing-docstring,C0103
import pytest
from unittest.mock import MagicMock
from requests.exceptions import ContentDecodingError
from freqtrade.exchange.bittrex import Bittrex
import freqtrade.exchange.bittrex as btx
# 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(mocker):
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(mocker):
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(mocker):
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}} # incomplete result
with pytest.raises(ContentDecodingError, match=r'.*Got 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')
def test_exchange_bittrex_get_ticker_history_one():
wb = make_wrap_bittrex()
FakeBittrex()
assert wb.get_ticker_history('BTC_ETH', 1)
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'.*Cannot parse 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'.*Got 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

@@ -0,0 +1,177 @@
# pragma pylint: disable=missing-docstring,W0212
import logging
import math
import pandas as pd
from unittest.mock import MagicMock
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.optimize import preprocess
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe
import freqtrade.optimize.backtesting as backtesting
def test_generate_text_table():
results = pd.DataFrame(
{
'currency': ['BTC_ETH', 'BTC_ETH'],
'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4],
'duration': [10, 30],
'profit': [2, 0],
'loss': [0, 0]
}
)
print(generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5))
assert generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5) == (
'pair buy count avg profit % total profit BTC avg duration profit loss\n' # noqa
'------- ----------- -------------- ------------------ -------------- -------- ------\n' # noqa
'BTC_ETH 2 15.00 0.60000000 100.0 2 0\n' # noqa
'TOTAL 2 15.00 0.60000000 100.0 2 0') # noqa
def test_get_timeframe():
data = preprocess(optimize.load_data(
None, ticker_interval=1, pairs=['BTC_UNITEST']))
min_date, max_date = 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'
def test_backtest(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
results = backtest(default_conf['stake_amount'],
optimize.preprocess(data), 10, True)
assert not results.empty
def test_backtest_1min_ticker_interval(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
results = backtest(default_conf['stake_amount'],
optimize.preprocess(data), 1, True)
assert not results.empty
def trim_dictlist(dl, num):
new = {}
for pair, pair_data in dl.items():
new[pair] = pair_data[num:]
return new
def load_data_test(what):
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = trim_dictlist(data, -100)
pair = data['BTC_UNITEST']
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}]
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)]}
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)]}
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 data
def simple_backtest(config, contour, num_results):
data = load_data_test(contour)
processed = optimize.preprocess(data)
assert isinstance(processed, dict)
results = backtest(config['stake_amount'], processed, 1, True)
# results :: <class 'pandas.core.frame.DataFrame'>
assert len(results) == num_results
# Test backtest on offline data
# loaded by freqdata/optimize/__init__.py::load_data()
def test_backtest2(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
results = backtest(default_conf['stake_amount'],
optimize.preprocess(data), 10, True)
assert not results.empty
def test_processed(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
dict_of_tickerrows = load_data_test('raise')
dataframes = optimize.preprocess(dict_of_tickerrows)
dataframe = dataframes['BTC_UNITEST']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
for col in ['close', 'high', 'low', 'open', 'date',
'ema50', 'ao', 'macd', 'plus_dm']:
assert col in cols
def test_backtest_pricecontours(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres)
def mocked_load_data(datadir, pairs=[], ticker_interval=0, refresh_pairs=False):
tickerdata = optimize.load_tickerdata_file(datadir, 'BTC_UNITEST', 1)
pairdata = {'BTC_UNITEST': tickerdata}
return trim_dictlist(pairdata, -100)
def test_backtest_start(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.misc.load_config', new=lambda s: default_conf)
mocker.patch.multiple('freqtrade.optimize',
load_data=mocked_load_data)
args = MagicMock()
args.ticker_interval = 1
args.level = 10
args.live = False
args.datadir = None
backtesting.start(args)
# check the logs, that will contain the backtest result
exists = ['Using max_open_trades: 1 ...',
'Using stake_amount: 0.001 ...',
'Measuring data from 2017-11-14T21:17:00+00:00 up to 2017-11-14T22:59:00+00:00 ...']
for line in exists:
assert ('freqtrade.optimize.backtesting',
logging.INFO,
line) in caplog.record_tuples

View File

@@ -0,0 +1,223 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
from freqtrade.optimize.hyperopt import calculate_loss, TARGET_TRADES, EXPECTED_MAX_PROFIT, start, \
log_results, save_trials, read_trials
def test_loss_calculation_prefer_correct_trade_count():
correct = calculate_loss(1, TARGET_TRADES, 20)
over = calculate_loss(1, TARGET_TRADES + 100, 20)
under = calculate_loss(1, TARGET_TRADES - 100, 20)
assert over > correct
assert under > correct
def test_loss_calculation_prefer_shorter_trades():
shorter = calculate_loss(1, 100, 20)
longer = calculate_loss(1, 100, 30)
assert shorter < longer
def test_loss_calculation_has_limited_profit():
correct = calculate_loss(EXPECTED_MAX_PROFIT, TARGET_TRADES, 20)
over = calculate_loss(EXPECTED_MAX_PROFIT * 2, TARGET_TRADES, 20)
under = calculate_loss(EXPECTED_MAX_PROFIT / 2, TARGET_TRADES, 20)
assert over == correct
assert under > correct
def create_trials(mocker):
"""
When creating trials, mock the hyperopt Trials so that *by default*
- we don't create any pickle'd files in the filesystem
- we might have a pickle'd file so make sure that we return
false when looking for it
"""
mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
return_value='freqtrade/tests/optimize/ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists',
return_value=False)
mocker.patch('freqtrade.optimize.hyperopt.save_trials',
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.read_trials',
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.os.remove',
return_value=True)
return mocker.Mock(
results=[{
'loss': 1,
'result': 'foo',
'status': 'ok'
}],
best_trial={'misc': {'vals': {'adx': 999}}}
)
def test_start_calls_fmin(mocker):
trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.TRIALS', return_value=trials)
mocker.patch('freqtrade.optimize.hyperopt.sorted',
return_value=trials.results)
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False)
start(args)
mock_fmin.assert_called_once()
def test_start_uses_mongotrials(mocker):
mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials',
return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True)
start(args)
mock_mongotrials.assert_called_once()
def test_log_results_if_loss_improves(mocker):
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
global CURRENT_BEST_LOSS
CURRENT_BEST_LOSS = 2
log_results({
'loss': 1,
'current_tries': 1,
'total_tries': 2,
'result': 'foo'
})
logger.assert_called_once()
def test_no_log_if_loss_does_not_improve(mocker):
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
global CURRENT_BEST_LOSS
CURRENT_BEST_LOSS = 2
log_results({
'loss': 3,
})
assert not logger.called
def test_fmin_best_results(mocker, caplog):
fmin_result = {
"adx": 1,
"adx-value": 15.0,
"fastd": 1,
"fastd-value": 40.0,
"green_candle": 1,
"mfi": 0,
"over_sar": 0,
"rsi": 1,
"rsi-value": 37.0,
"trigger": 2,
"uptrend_long_ema": 1,
"uptrend_short_ema": 0,
"uptrend_sma": 0,
"stoploss": -0.1,
}
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
args = mocker.Mock(epochs=1, config='config.json.example')
start(args)
exists = [
'Best parameters',
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
'"green_candle": {\n "enabled": true\n },',
'"mfi": {\n "enabled": false\n },',
'"trigger": {\n "type": "ao_cross_zero"\n },',
'"stoploss": -0.1',
]
for line in exists:
assert line in caplog.text
def test_fmin_throw_value_error(mocker, caplog):
mocker.patch('freqtrade.optimize.hyperopt.MongoTrials', return_value=create_trials(mocker))
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
args = mocker.Mock(epochs=1, config='config.json.example')
start(args)
exists = [
'Best Result:',
'Sorry, Hyperopt was not able to find good parameters. Please try with more epochs '
'(param: -e).',
]
for line in exists:
assert line in caplog.text
def test_resuming_previous_hyperopt_results_succeeds(mocker):
import freqtrade.optimize.hyperopt as hyperopt
trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.TRIALS',
return_value=trials)
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists',
return_value=True)
mocker.patch('freqtrade.optimize.hyperopt.len',
return_value=len(trials.results))
mock_read = mocker.patch('freqtrade.optimize.hyperopt.read_trials',
return_value=trials)
mock_save = mocker.patch('freqtrade.optimize.hyperopt.save_trials',
return_value=None)
mocker.patch('freqtrade.optimize.hyperopt.sorted',
return_value=trials.results)
mocker.patch('freqtrade.optimize.preprocess')
mocker.patch('freqtrade.optimize.load_data')
mocker.patch('freqtrade.optimize.hyperopt.fmin',
return_value={})
args = mocker.Mock(epochs=1,
config='config.json.example',
mongodb=False)
start(args)
mock_read.assert_called_once()
mock_save.assert_called_once()
current_tries = hyperopt._CURRENT_TRIES
total_tries = hyperopt.TOTAL_TRIES
assert current_tries == len(trials.results)
assert total_tries == (current_tries + len(trials.results))
def test_save_trials_saves_trials(mocker):
trials = create_trials(mocker)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump',
return_value=None)
trials_path = mocker.patch('freqtrade.optimize.hyperopt.TRIALS_FILE',
return_value='ut_trials.pickle')
mocker.patch('freqtrade.optimize.hyperopt.open',
return_value=trials_path)
save_trials(trials, trials_path)
mock_dump.assert_called_once_with(trials, trials_path)
def test_read_trials_returns_trials_file(mocker):
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)
assert read_trials() == trials
mock_open.assert_called_once()
mock_load.assert_called_once()

View File

@@ -0,0 +1,16 @@
# pragma pylint: disable=missing-docstring,W0212
from freqtrade.optimize.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

@@ -0,0 +1,176 @@
# pragma pylint: disable=missing-docstring,W0212
import os
import logging
from shutil import copyfile
from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs,\
download_backtesting_testdata, load_tickerdata_file
# Change this if modifying BTC_UNITEST testdatafile
_btc_unittest_length = 13681
def _backup_file(file: str, copy_file: bool = False) -> None:
"""
Backup existing file to avoid deleting the user file
:param file: complete path to the file
:param touch_file: create an empty file in replacement
:return: None
"""
file_swp = file + '.swp'
if os.path.isfile(file):
os.rename(file, file_swp)
if copy_file:
copyfile(file_swp, file)
def _clean_test_file(file: str) -> None:
"""
Backup existing file to avoid deleting the user file
:param file: complete path to the file
:return: None
"""
file_swp = file + '.swp'
# 1. Delete file from the test
if os.path.isfile(file):
os.remove(file)
# 2. Rollback to the initial file
if os.path.isfile(file_swp):
os.rename(file_swp, file)
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
_backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['BTC_ETH'])
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_ETH", Interval: 5 min'
) not in caplog.record_tuples
_clean_test_file(file)
def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
_backup_file(file, copy_file=True)
optimize.load_data(None, ticker_interval=1, pairs=['BTC_ETH'])
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_ETH", Interval: 1 min'
) not in caplog.record_tuples
_clean_test_file(file)
def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
_backup_file(file)
optimize.load_data(None, ticker_interval=1, pairs=['BTC_MEME'])
assert os.path.isfile(file) is True
assert ('freqtrade.optimize',
logging.INFO,
'Download the pair: "BTC_MEME", Interval: 1 min'
) in caplog.record_tuples
_clean_test_file(file)
def test_testdata_path():
assert os.path.join('freqtrade', 'tests', 'testdata') in make_testdata_path(None)
def test_download_pairs(default_conf, ticker_history, mocker):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
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'
_backup_file(file1_1)
_backup_file(file1_5)
_backup_file(file2_1)
_backup_file(file2_5)
assert download_pairs(None, pairs=['BTC-MEME', 'BTC-CFI']) is True
assert os.path.isfile(file1_1) is True
assert os.path.isfile(file1_5) is True
assert os.path.isfile(file2_1) is True
assert os.path.isfile(file2_5) is True
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file1_5)
_clean_test_file(file2_1)
_clean_test_file(file2_5)
def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
side_effect=BaseException('File Error'))
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
_backup_file(file1_1)
_backup_file(file1_5)
download_pairs(None, pairs=['BTC-MEME'])
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file1_5)
assert ('freqtrade.optimize.__init__',
logging.INFO,
'Failed to download the pair: "BTC-MEME", Interval: 1 min'
) in caplog.record_tuples
def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Download a 1 min ticker file
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
_backup_file(file1)
download_backtesting_testdata(None, pair="BTC-XEL", interval=1)
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'
_backup_file(file2)
download_backtesting_testdata(None, pair="BTC-STORJ", interval=5)
assert os.path.isfile(file2) is True
_clean_test_file(file2)
def test_load_tickerdata_file():
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1)
assert _btc_unittest_length == len(tickerdata)

View File

@@ -0,0 +1,57 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
from copy import deepcopy
from unittest.mock import MagicMock
from freqtrade.rpc import init, cleanup, send_msg
def test_init_telegram_enabled(default_conf, mocker):
module_list = []
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
init(default_conf)
assert telegram_mock.call_count == 1
assert 'telegram' in module_list
def test_init_telegram_disabled(default_conf, mocker):
module_list = []
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
init(conf)
assert telegram_mock.call_count == 0
assert 'telegram' not in module_list
def test_cleanup_telegram_enabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
cleanup()
assert telegram_mock.call_count == 1
def test_cleanup_telegram_disabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
cleanup()
assert telegram_mock.call_count == 0
def test_send_msg_telegram_enabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
send_msg('test')
assert telegram_mock.call_count == 1
def test_send_msg_telegram_disabled(mocker):
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
send_msg('test')
assert telegram_mock.call_count == 0

View File

@@ -1,10 +1,9 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
import re
from datetime import datetime
from random import randint
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine
from telegram import Update, Message, Chat
from telegram.error import NetworkError
@@ -14,10 +13,9 @@ from freqtrade.main import init, create_trade
from freqtrade.misc import update_state, State, get_state
from freqtrade.persistence import Trade
from freqtrade.rpc import telegram
from freqtrade.rpc.telegram import (
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance,
authorized_only, _help, is_enabled, send_msg,
_version)
from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
_profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help, \
_exec_forcesell
def test_is_enabled(default_conf, mocker):
@@ -79,9 +77,10 @@ def test_authorized_only_exception(default_conf, mocker):
def test_status_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -103,24 +102,21 @@ def test_status_handle(default_conf, update, ticker, mocker):
msg_mock.reset_mock()
# Create some test data
trade = create_trade(15.0)
assert trade
Trade.session.add(trade)
Trade.session.flush()
create_trade(0.001)
# Trigger status while we have a fulfilled order for the open trade
_status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
assert msg_mock.call_count == 1
assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0]
def test_status_table_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock()
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple(
'freqtrade.main.telegram',
'freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -142,10 +138,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
msg_mock.reset_mock()
# Create some test data
trade = create_trade(15.0)
assert trade
Trade.session.add(trade)
Trade.session.flush()
create_trade(15.0)
_status_table(bot=MagicMock(), update=update)
@@ -155,20 +148,26 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
assert int(fields[0]) == 1
assert fields[1] == 'BTC_ETH'
assert msg_mock.call_count == 2
assert msg_mock.call_count == 1
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
def test_profit_handle(
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
init(default_conf, create_engine('sqlite://'))
_profit(bot=MagicMock(), update=update)
@@ -177,92 +176,172 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
msg_mock.reset_mock()
# Create some test data
trade = create_trade(15.0)
assert trade
create_trade(0.001)
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
_profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2
assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock()
# Simulate fulfilled LIMIT_SELL order for trade
# Update the ticker with a market going up
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert '*ROI:* `1.50701325 (10.05%)`' in msg_mock.call_args_list[-1][0][0]
assert 'Best Performing:* `BTC_ETH: 10.05%`' in msg_mock.call_args_list[-1][0][0]
assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0]
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 '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
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]
def test_forcesell_handle(default_conf, update, ticker, mocker):
def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
trade = create_trade(15.0)
create_trade(0.001)
trade = Trade.query.first()
assert trade
Trade.session.add(trade)
Trade.session.flush()
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
update.message.text = '/forcesell 1'
_forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
assert '0.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0]
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' 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):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
# Decrease the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_down)
trade = Trade.query.first()
assert trade
update.message.text = '/forcesell 1'
_forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' 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_exec_forcesell_open_orders(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.exchange',
get_ticker=ticker,
get_order=MagicMock(return_value={
'closed': None,
'type': 'LIMIT_BUY',
}),
cancel_order=cancel_order_mock)
trade = Trade(
pair='BTC_ETH',
open_rate=1,
exchange='BITTREX',
open_order_id='123456789',
amount=1,
fee=0.0,
)
_exec_forcesell(trade)
assert cancel_order_mock.call_count == 1
assert trade.is_open is False
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
for _ in range(4):
Trade.session.add(create_trade(15.0))
Trade.session.flush()
msg_mock.reset_mock()
create_trade(0.001)
rpc_mock.reset_mock()
update.message.text = '/forcesell all'
_forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 4
for args in msg_mock.call_args_list:
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
assert rpc_mock.call_count == 4
for args in rpc_mock.call_args_list:
assert '0.00001098' in args[0][0]
assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0]
assert '-0.089 USD' in args[0][0]
def test_forcesell_handle_invalid(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -297,9 +376,10 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
def test_performance_handle(
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -309,7 +389,8 @@ def test_performance_handle(
init(default_conf, create_engine('sqlite://'))
# Create some test data
trade = create_trade(15.0)
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
@@ -320,21 +401,69 @@ def test_performance_handle(
trade.close_date = datetime.utcnow()
trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 2
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
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]
def test_daily_handle(
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock()
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
# Try valid data
update.message.text = '/daily 2'
_daily(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'Daily' in msg_mock.call_args_list[0][0][0]
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
# Try invalid data
msg_mock.reset_mock()
update_state(State.RUNNING)
update.message.text = '/daily -2'
_daily(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0]
def test_count_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.main.telegram',
'freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -351,9 +480,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
update_state(State.RUNNING)
# Create some test data
Trade.session.add(create_trade(15.0))
Trade.session.flush()
create_trade(0.001)
msg_mock.reset_mock()
_count(bot=MagicMock(), update=update)
@@ -365,9 +492,9 @@ def test_count_handle(default_conf, update, ticker, mocker):
def test_performance_handle_invalid(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -385,7 +512,7 @@ def test_performance_handle_invalid(default_conf, update, mocker):
def test_start_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -403,7 +530,7 @@ def test_start_handle(default_conf, update, mocker):
def test_start_handle_already_running(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -422,7 +549,7 @@ def test_start_handle_already_running(default_conf, update, mocker):
def test_stop_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -441,7 +568,7 @@ def test_stop_handle(default_conf, update, mocker):
def test_stop_handle_already_stopped(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -473,7 +600,7 @@ def test_balance_handle(default_conf, update, mocker):
}]
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -489,7 +616,7 @@ def test_balance_handle(default_conf, update, mocker):
def test_help_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -502,7 +629,7 @@ def test_help_handle(default_conf, update, mocker):
def test_version_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
@@ -514,12 +641,12 @@ def test_version_handle(default_conf, update, mocker):
def test_send_msg(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
bot = MagicMock()
send_msg('test', bot)
assert len(bot.method_calls) == 0
assert not bot.method_calls
bot.reset_mock()
default_conf['telegram']['enabled'] = True
@@ -529,13 +656,12 @@ def test_send_msg(default_conf, mocker):
def test_send_msg_network_error(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
default_conf['telegram']['enabled'] = True
bot = MagicMock()
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
with pytest.raises(NetworkError, match=r'Oh snap'):
send_msg('test', bot)
# Bot should've tried to send it twice

View File

@@ -0,0 +1,141 @@
from freqtrade.main import refresh_whitelist, gen_pair_whitelist
# whitelist, blacklist, filtering, all of that will
# eventually become some rules to run on a generic ACL engine
# perhaps try to anticipate that by using some python package
def whitelist_conf():
return {
'stake_currency': 'BTC',
'exchange': {
'pair_whitelist': [
'BTC_ETH',
'BTC_TKN',
'BTC_TRST',
'BTC_SWT',
'BTC_BCC'
],
'pair_blacklist': [
'BTC_BLK'
],
},
}
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):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health)
refreshedwhitelist = refresh_whitelist(
conf['exchange']['pair_whitelist'] + ['BTC_XXX'])
# List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN']
# Ensure all except those in whitelist are removed
assert whitelist == refreshedwhitelist
def test_refresh_whitelist(mocker):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health)
refreshedwhitelist = refresh_whitelist(conf['exchange']['pair_whitelist'])
# List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN']
# Ensure all except those in whitelist are removed
assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic(mocker):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health)
mocker.patch.multiple('freqtrade.main.exchange',
get_market_summaries=get_market_summaries)
# argument: use the whitelist dynamically by exchange-volume
whitelist = ['BTC_TKN', 'BTC_ETH']
refreshedwhitelist = refresh_whitelist(
gen_pair_whitelist(conf['stake_currency']))
assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic_empty(mocker):
conf = whitelist_conf()
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.exchange',
get_wallet_health=get_health_empty)
# argument: use the whitelist dynamically by exchange-volume
whitelist = []
conf['exchange']['pair_whitelist'] = []
refresh_whitelist(whitelist)
pairslist = conf['exchange']['pair_whitelist']
assert set(whitelist) == set(pairslist)

View File

@@ -1,16 +1,19 @@
# pragma pylint: disable=missing-docstring
from datetime import datetime
# pragma pylint: disable=missing-docstring,W0621
import json
from unittest.mock import MagicMock
import arrow
import pytest
from pandas import DataFrame
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
get_buy_signal
from freqtrade.analyze import (SignalType, get_signal, parse_ticker_dataframe,
populate_buy_trend, populate_indicators,
populate_sell_trend)
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/btc-eth.json') as data_file:
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
return parse_ticker_dataframe(json.load(data_file))
@@ -20,20 +23,52 @@ def test_dataframe_correct_columns(result):
def test_dataframe_correct_length(result):
assert len(result.index) == 5751
assert len(result.index) == 14395
def test_populates_buy_trend(result):
dataframe = populate_buy_trend(populate_indicators(result))
assert 'buy' in dataframe.columns
assert 'buy_price' in dataframe.columns
def test_populates_sell_trend(result):
dataframe = populate_sell_trend(populate_indicators(result))
assert 'sell' in dataframe.columns
def test_returns_latest_buy_signal(mocker):
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
assert get_buy_signal('BTC-ETH')
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
)
assert get_signal('BTC-ETH', SignalType.BUY)
buydf = DataFrame([{'buy': 0, 'date': datetime.today()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
assert not get_buy_signal('BTC-ETH')
mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
)
assert not get_signal('BTC-ETH', SignalType.BUY)
def test_returns_latest_sell_signal(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
)
assert get_signal('BTC-ETH', SignalType.SELL)
mocker.patch(
'freqtrade.analyze.analyze_ticker',
return_value=DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
)
assert not get_signal('BTC-ETH', SignalType.SELL)
def test_get_signal_handles_exceptions(mocker):
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
mocker.patch('freqtrade.analyze.analyze_ticker',
side_effect=Exception('invalid ticker history '))
assert not get_signal('BTC-ETH', SignalType.BUY)

View File

@@ -1,65 +0,0 @@
# pragma pylint: disable=missing-docstring
import logging
import os
import pytest
import arrow
from pandas import DataFrame
from freqtrade import exchange
from freqtrade.analyze import analyze_ticker
from freqtrade.exchange import Bittrex
from freqtrade.main import should_sell
from freqtrade.persistence import Trade
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
def format_results(results):
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
len(results.index), results.profit.mean() * 100.0, results.profit.sum(), results.duration.mean() * 5)
def print_pair_results(pair, results):
print('For currency {}:'.format(pair))
print(format_results(results[results.currency == pair]))
def backtest(backtest_conf, backdata, mocker):
trades = []
exchange._API = Bittrex({'key': '', 'secret': ''})
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
for pair, pair_data in backdata.items():
mocked_history.return_value = pair_data
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
# for each buy point
for row in ticker[ticker.buy == 1].itertuples(index=True):
trade = Trade(
open_rate=row.close,
open_date=row.date,
amount=1,
fee=exchange.get_fee() * 2
)
# calculate win/lose forwards from buy point
for row2 in ticker[row.Index:].itertuples(index=True):
if should_sell(trade, row2.close, row2.date):
current_profit = trade.calc_profit(row2.close)
trades.append((pair, current_profit, row2.Index - row.Index))
break
labels = ['currency', 'profit', 'duration']
results = DataFrame.from_records(trades, columns=labels)
return results
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_backtest(backtest_conf, backdata, mocker, report=True):
results = backtest(backtest_conf, backdata, mocker)
print('====================== BACKTESTING REPORT ================================')
for pair in backdata:
print_pair_results(pair, results)
print('TOTAL OVER ALL TRADES:')
print(format_results(results))

View File

@@ -0,0 +1,27 @@
import pandas
import freqtrade.optimize
from freqtrade import analyze
_pairs = ['BTC_ETH']
def load_dataframe_pair(pairs):
ld = freqtrade.optimize.load_data(None, ticker_interval=5, pairs=pairs)
assert isinstance(ld, dict)
assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]]
dataframe = analyze.analyze_ticker(dataframe)
return dataframe
def test_dataframe_load():
dataframe = load_dataframe_pair(_pairs)
assert isinstance(dataframe, pandas.core.frame.DataFrame)
def test_dataframe_columns_exists():
dataframe = load_dataframe_pair(_pairs)
assert 'high' in dataframe.columns
assert 'low' in dataframe.columns
assert 'close' in dataframe.columns

View File

@@ -1,36 +0,0 @@
# pragma pylint: disable=missing-docstring
from unittest.mock import MagicMock
import pytest
from freqtrade.exchange import validate_pairs
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'])
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)
with pytest.raises(RuntimeError, match=r'not available'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
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)
with pytest.raises(RuntimeError, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist'])

View File

@@ -0,0 +1,125 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
import time
from unittest.mock import MagicMock
import pytest
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
def test_pair_convertion_object():
pair_convertion = CryptoFiat(
crypto_symbol='btc',
fiat_symbol='usd',
price=12345.0
)
# Check the cache duration is 6 hours
assert pair_convertion.CACHE_DURATION == 6 * 60 * 60
# Check a regular usage
assert pair_convertion.crypto_symbol == 'BTC'
assert pair_convertion.fiat_symbol == 'USD'
assert pair_convertion.price == 12345.0
assert pair_convertion.is_expired() is False
# Update the expiration time (- 2 hours) and check the behavior
pair_convertion._expiration = time.time() - 2 * 60 * 60
assert pair_convertion.is_expired() is True
# Check set price behaviour
time_reference = time.time() + pair_convertion.CACHE_DURATION
pair_convertion.set_price(price=30000.123)
assert pair_convertion.is_expired() is False
assert pair_convertion._expiration >= time_reference
assert pair_convertion.price == 30000.123
def test_fiat_convert_is_supported():
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._is_supported_fiat(fiat='USD') is True
assert fiat_convert._is_supported_fiat(fiat='usd') is True
assert fiat_convert._is_supported_fiat(fiat='abc') is False
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
def test_fiat_convert_add_pair():
fiat_convert = CryptoToFiatConverter()
assert len(fiat_convert._pairs) == 0
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0)
assert len(fiat_convert._pairs) == 1
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
assert fiat_convert._pairs[0].price == 12345.0
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2)
assert len(fiat_convert._pairs) == 2
assert fiat_convert._pairs[1].crypto_symbol == 'BTC'
assert fiat_convert._pairs[1].fiat_symbol == 'EUR'
assert fiat_convert._pairs[1].price == 13000.2
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.Pymarketcap.ticker', api_mock)
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')
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
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=13000.2)
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
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.Pymarketcap.ticker', api_mock)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0)
fiat_convert = CryptoToFiatConverter()
with pytest.raises(ValueError, match=r'The fiat US DOLLAR is not supported.'):
fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='US Dollar')
# Check the value return by the method
assert len(fiat_convert._pairs) == 0
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
assert fiat_convert._pairs[0].price == 28000.0
assert fiat_convert._pairs[0]._expiration is not 0
assert len(fiat_convert._pairs) == 1
# Verify the cached is used
fiat_convert._pairs[0].price = 9867.543
expiration = fiat_convert._pairs[0]._expiration
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 9867.543
assert fiat_convert._pairs[0]._expiration == expiration
# Verify the cache expiration
expiration = time.time() - 2 * 60 * 60
fiat_convert._pairs[0]._expiration = expiration
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
assert fiat_convert._pairs[0]._expiration is not expiration
def test_fiat_convert_without_network(mocker):
pymarketcap = MagicMock(side_effect=ImportError('Oh boy, you have no network!'))
mocker.patch('freqtrade.fiat_convert.Pymarketcap', pymarketcap)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._coinmarketcap is None
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0

View File

@@ -1,148 +0,0 @@
# pragma pylint: disable=missing-docstring
import logging
import os
from functools import reduce
from math import exp
from operator import itemgetter
import pytest
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
from pandas import DataFrame
from freqtrade.tests.test_backtesting import backtest, format_results
from freqtrade.vendor.qtpylib.indicators import crossed_above
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
TARGET_TRADES = 1300
TOTAL_TRIES = 4
current_tries = 0
def buy_strategy_generator(params):
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if params['uptrend_long_ema']['enabled']:
conditions.append(dataframe['ema50'] > dataframe['ema100'])
if params['uptrend_short_ema']['enabled']:
conditions.append(dataframe['ema5'] > dataframe['ema10'])
if params['mfi']['enabled']:
conditions.append(dataframe['mfi'] < params['mfi']['value'])
if params['fastd']['enabled']:
conditions.append(dataframe['fastd'] < params['fastd']['value'])
if params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
if params['over_sar']['enabled']:
conditions.append(dataframe['close'] > dataframe['sar'])
if params['green_candle']['enabled']:
conditions.append(dataframe['close'] > dataframe['open'])
if params['uptrend_sma']['enabled']:
prevsma = dataframe['sma'].shift(1)
conditions.append(dataframe['sma'] > prevsma)
# TRIGGERS
triggers = {
'lower_bb': dataframe['tema'] <= dataframe['blower'],
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
}
conditions.append(triggers.get(params['trigger']['type']))
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe
return populate_buy_trend
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_hyperopt(backtest_conf, backdata, mocker):
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
def optimizer(params):
mocked_buy_trend.side_effect = buy_strategy_generator(params)
results = backtest(backtest_conf, backdata, mocker)
result = format_results(results)
total_profit = results.profit.sum() * 1000
trade_count = len(results.index)
trade_loss = 1 - 0.4 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = max(0, 1 - total_profit / 15000) # max profit 15000
global current_tries
current_tries += 1
print('{}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
return {
'loss': trade_loss + profit_loss,
'status': STATUS_OK,
'result': result
}
space = {
'mfi': hp.choice('mfi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
]),
'fastd': hp.choice('fastd', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
{'enabled': False},
{'enabled': True}
]),
'over_sar': hp.choice('over_sar', [
{'enabled': False},
{'enabled': True}
]),
'green_candle': hp.choice('green_candle', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_sma': hp.choice('uptrend_sma', [
{'enabled': False},
{'enabled': True}
]),
'trigger': hp.choice('trigger', [
{'type': 'lower_bb'},
{'type': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]),
}
trials = Trials()
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials)
print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================')
print('Best parameters {}'.format(best))
newlist = sorted(trials.results, key=itemgetter('loss'))
print('Result: {}'.format(newlist[0]['result']))

View File

@@ -1,31 +1,68 @@
# pragma pylint: disable=missing-docstring
# pragma pylint: disable=missing-docstring,C0103
import copy
import logging
from unittest.mock import MagicMock
import arrow
import pytest
import requests
from sqlalchemy import create_engine
import freqtrade.main as main
from freqtrade import DependencyException, OperationalException
from freqtrade.analyze import SignalType
from freqtrade.exchange import Exchanges
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
get_target_bid, _process
from freqtrade.misc import get_state, State
from freqtrade.main import (_process, check_handle_timedout, create_trade,
execute_sell, get_target_bid, handle_trade, init)
from freqtrade.misc import State, get_state
from freqtrade.persistence import Trade
def test_process_trade_creation(default_conf, ticker, health, mocker):
def test_parse_args_backtesting(mocker):
""" Test that main() can start backtesting or hyperopt.
and also ensure we can pass some specific arguments
argument parsing is done in test_misc.py """
backtesting_mock = mocker.patch(
'freqtrade.optimize.backtesting.start', MagicMock())
with pytest.raises(SystemExit, match=r'0'):
main.main(['backtesting'])
assert backtesting_mock.call_count == 1
call_args = backtesting_mock.call_args[0][0]
assert call_args.config == 'config.json'
assert call_args.live is False
assert call_args.loglevel == 20
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 5
def test_main_start_hyperopt(mocker):
hyperopt_mock = mocker.patch(
'freqtrade.optimize.hyperopt.start', MagicMock())
with pytest.raises(SystemExit, match=r'0'):
main.main(['hyperopt'])
assert hyperopt_mock.call_count == 1
call_args = hyperopt_mock.call_args[0][0]
assert call_args.config == 'config.json'
assert call_args.loglevel == 20
assert call_args.subparser == 'hyperopt'
assert call_args.func is not None
def test_process_trade_creation(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'))
buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order))
init(default_conf, create_engine('sqlite://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 0
assert not trades
result = _process()
assert result is True
@@ -38,14 +75,14 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
assert trade.open_rate == 0.072661
assert trade.amount == 0.6864067381401302
assert trade.open_rate == 0.00001099
assert trade.amount == 90.99181073703367
def test_process_exchange_failures(default_conf, ticker, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
@@ -58,29 +95,30 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
assert sleep_mock.has_calls()
def test_process_runtime_error(default_conf, ticker, health, mocker):
def test_process_operational_exception(default_conf, ticker, health, mocker):
msg_mock = MagicMock()
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=RuntimeError))
buy=MagicMock(side_effect=OperationalException))
init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING
result = _process()
assert result is False
assert get_state() == State.STOPPED
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0]
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal',
side_effect=lambda *args: False if args[1] == SignalType.SELL else True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
@@ -90,7 +128,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
init(default_conf, create_engine('sqlite://'))
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 0
assert not trades
result = _process()
assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
@@ -102,8 +140,8 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
@@ -112,11 +150,11 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0)
Trade.session.add(trade)
Trade.session.flush()
create_trade(0.001)
trade = Trade.query.first()
assert trade is not None
assert trade.stake_amount == 15.0
assert trade.stake_amount == 0.001
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
@@ -124,78 +162,176 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
assert trade.open_rate == 0.07256061
assert trade.amount == 206.43811673387373
assert trade.open_rate == 0.00001099
assert trade.amount == 90.99181073
assert whitelist == default_conf['exchange']['pair_whitelist']
def test_create_trade_minimal_amount(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
buy_mock = mocker.patch(
'freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy')
)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
init(default_conf, create_engine('sqlite://'))
min_stake_amount = 0.0005
create_trade(min_stake_amount)
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
assert rate * amount >= min_stake_amount
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
with pytest.raises(ValueError, match=r'.*stake amount.*'):
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
create_trade(default_conf['stake_amount'])
def test_create_trade_no_pairs(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
with pytest.raises(ValueError, match=r'.*No pair in whitelist.*'):
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = []
mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount'])
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount'])
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.17256061,
'ask': 0.172661,
'last': 0.17256061
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value='mocked_limit_buy'),
sell=MagicMock(return_value='mocked_limit_sell'))
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0)
trade.update(limit_buy_order)
Trade.session.add(trade)
Trade.session.flush()
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
create_trade(0.001)
trade = Trade.query.first()
assert trade
trade.update(limit_buy_order)
assert trade.is_open is True
handle_trade(trade)
assert trade.open_order_id == 'mocked_limit_sell'
assert close_trade_if_fulfilled(trade) is False
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
assert trade.close_rate == 0.0802134
assert trade.close_profit == 0.10046755
assert trade.close_rate == 0.00001173
assert trade.close_profit == 0.06201057
assert trade.calc_profit() == 0.00006217
assert trade.close_date is not None
def test_handle_trade_roi(default_conf, ticker, mocker, caplog):
default_conf.update({'experimental': {'use_sell_signal': True}})
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
mocker.patch('freqtrade.main.min_roi_reached', return_value=True)
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.is_open = True
# FIX: sniffing logs, suggest handle_trade should not execute_sell
# instead that responsibility should be moved out of handle_trade(),
# we might just want to check if we are in a sell condition without
# executing
# if ROI is reached we must sell
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
assert handle_trade(trade)
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
# if ROI is reached we must sell even if sell-signal is not signalled
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
assert handle_trade(trade)
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
def test_handle_trade_experimental(default_conf, ticker, mocker, caplog):
default_conf.update({'experimental': {'use_sell_signal': True}})
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.is_open = True
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
value_returned = handle_trade(trade)
assert ('freqtrade', logging.DEBUG, 'Checking sell_signal ...') in caplog.record_tuples
assert value_returned is False
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
assert handle_trade(trade)
s = 'Executing sell due to sell signal ...'
assert ('freqtrade', logging.DEBUG, s) in caplog.record_tuples
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
@@ -203,25 +339,119 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
# Create trade and sell it
init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0)
trade.update(limit_buy_order)
trade.update(limit_sell_order)
create_trade(0.001)
Trade.session.add(trade)
Trade.session.flush()
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
trade = Trade.query.first()
assert trade
# Simulate that there is no open order
trade.open_order_id = None
trade.update(limit_buy_order)
trade.update(limit_sell_order)
assert trade.is_open is False
closed = close_trade_if_fulfilled(trade)
assert closed
assert not trade.is_open
with pytest.raises(ValueError, match=r'.*closed trade.*'):
handle_trade(trade)
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
trade_buy = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
)
Trade.session.add(trade_buy)
# check it does cancel buy orders over the time limit
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
assert len(trades) == 0
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
trade_sell = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(hours=-5).datetime,
close_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=False
)
Trade.session.add(trade_sell)
# check it does cancel sell orders over the time limit
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
assert trade_sell.is_open is True
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order=cancel_order_mock)
init(default_conf, create_engine('sqlite://'))
trade_buy = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
)
Trade.session.add(trade_buy)
# check it does cancel buy orders over the time limit
# note this is for a partially-complete buy order
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
assert len(trades) == 1
assert trades[0].amount == 23.0
assert trades[0].stake_amount == trade_buy.open_rate * trades[0].amount
def test_balance_fully_ask_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 20
@@ -235,3 +465,212 @@ def test_balance_fully_last_side(mocker):
def test_balance_bigger_last_ask(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 5, 'last': 10}) == 5
def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' 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_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
ticker=MagicMock(return_value={'price_usd': 15000.0}),
_cache_symbols=MagicMock(return_value={'BTC': 1}))
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Decrease the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_down)
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' 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_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
trade = Trade.query.first()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
mocker.patch('freqtrade.main._CONF', {})
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' 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 'USD' not in rpc_mock.call_args_list[-1][0][0]
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': True,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00002172,
'ask': 0.00002173,
'last': 0.00002172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.update(limit_buy_order)
assert handle_trade(trade) is True
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': False,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00002172,
'ask': 0.00002173,
'last': 0.00002172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.update(limit_buy_order)
assert handle_trade(trade) is True
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': True,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000172,
'ask': 0.00000173,
'last': 0.00000172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.update(limit_buy_order)
assert handle_trade(trade) is False
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
default_conf['experimental'] = {
'use_sell_signal': True,
'sell_profit_only': False,
}
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000172,
'ask': 0.00000173,
'last': 0.00000172
}),
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
trade.update(limit_buy_order)
assert handle_trade(trade) is True

View File

@@ -1,7 +1,14 @@
# pragma pylint: disable=missing-docstring
# pragma pylint: disable=missing-docstring,C0103
import argparse
import json
import time
from copy import deepcopy
from freqtrade.misc import throttle
import pytest
from jsonschema import ValidationError
from freqtrade.misc import (common_args_parser, load_config, parse_args,
throttle)
def test_throttle():
@@ -10,11 +17,148 @@ def test_throttle():
return 42
start = time.time()
result = throttle(func, 0.1)
result = throttle(func, min_secs=0.1)
end = time.time()
assert result == 42
assert end - start > 0.1
result = throttle(func, -1)
result = throttle(func, min_secs=-1)
assert result == 42
def test_throttle_with_assets():
def func(nb_assets=-1):
return nb_assets
result = throttle(func, min_secs=0.1, nb_assets=666)
assert result == 666
result = throttle(func, min_secs=0.1)
assert result == -1
# Parse common command-line-arguments. Used for all tools
def test_parse_args_none():
args = common_args_parser('')
assert isinstance(args, argparse.ArgumentParser)
def test_parse_args_defaults():
args = parse_args([], '')
assert args.config == 'config.json'
assert args.dynamic_whitelist is None
assert args.loglevel == 20
def test_parse_args_config():
args = parse_args(['-c', '/dev/null'], '')
assert args.config == '/dev/null'
args = parse_args(['--config', '/dev/null'], '')
assert args.config == '/dev/null'
def test_parse_args_verbose():
args = parse_args(['-v'], '')
assert args.loglevel == 10
args = parse_args(['--verbose'], '')
assert args.loglevel == 10
def test_parse_args_version():
with pytest.raises(SystemExit, match=r'0'):
parse_args(['--version'], '')
def test_parse_args_invalid():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['-c'], '')
# Parse command-line-arguments
# used for main, backtesting and hyperopt
def test_parse_args_dynamic_whitelist():
args = parse_args(['--dynamic-whitelist'], '')
assert args.dynamic_whitelist == 20
def test_parse_args_dynamic_whitelist_10():
args = parse_args(['--dynamic-whitelist', '10'], '')
assert args.dynamic_whitelist == 10
def test_parse_args_dynamic_whitelist_invalid_values():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['--dynamic-whitelist', 'abc'], '')
def test_parse_args_backtesting_invalid():
with pytest.raises(SystemExit, match=r'2'):
parse_args(['backtesting --ticker-interval'], '')
with pytest.raises(SystemExit, match=r'2'):
parse_args(['backtesting --ticker-interval', 'abc'], '')
def test_parse_args_backtesting_custom():
args = [
'-c', 'test_conf.json',
'backtesting',
'--live',
'--ticker-interval', '1',
'--refresh-pairs-cached']
call_args = parse_args(args, '')
assert call_args.config == 'test_conf.json'
assert call_args.live is True
assert call_args.loglevel == 20
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 1
assert call_args.refresh_pairs is True
def test_parse_args_hyperopt_custom(mocker):
args = ['-c', 'test_conf.json', 'hyperopt', '--epochs', '20']
call_args = parse_args(args, '')
assert call_args.config == 'test_conf.json'
assert call_args.epochs == 20
assert call_args.loglevel == 20
assert call_args.subparser == 'hyperopt'
assert call_args.func is not None
def test_load_config(default_conf, mocker):
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
validated_conf = load_config('somefile')
assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items()
def test_load_config_invalid_pair(default_conf, mocker):
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'].append('BTC-ETH')
mocker.patch(
'freqtrade.misc.open',
mocker.mock_open(
read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*does not match.*'):
load_config('somefile')
def test_load_config_missing_attributes(default_conf, mocker):
conf = deepcopy(default_conf)
conf.pop('exchange')
mocker.patch(
'freqtrade.misc.open',
mocker.mock_open(
read_data=json.dumps(conf)))
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
load_config('somefile')

View File

@@ -1,15 +1,126 @@
# pragma pylint: disable=missing-docstring
import os
import pytest
from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade
from freqtrade.persistence import Trade, init
def test_update(limit_buy_order, limit_sell_order):
def test_init_create_session(default_conf, mocker):
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
# Check if init create a session
init(default_conf)
assert hasattr(Trade, 'session')
assert type(Trade.session).__name__ is 'Session'
def test_init_dry_run_db(default_conf, mocker):
default_conf.update({'dry_run_db': True})
mocker.patch.dict('freqtrade.persistence._CONF', 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 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)
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)
# 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)
def test_init_prod_db(default_conf, mocker):
default_conf.update({'dry_run': False})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
# First, protect the existing 'tradesv3.sqlite' (Do not delete user data)
prod_db = 'tradesv3.sqlite'
prod_db_swp = prod_db + '.swp'
if os.path.isfile(prod_db):
os.rename(prod_db, prod_db_swp)
# 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
# 1. Delete file from the test
if os.path.isfile(prod_db):
os.remove(prod_db)
# Rollback to the initial 'tradesv3.sqlite' file
if os.path.isfile(prod_db_swp):
os.rename(prod_db_swp, prod_db)
def test_update_with_bittrex(limit_buy_order, limit_sell_order):
"""
On this test we will buy and sell a crypto currency.
Buy
- Buy: 90.99181073 Crypto at 0.00001099 BTC
(90.99181073*0.00001099 = 0.0009999 BTC)
- Buying fee: 0.25%
- Total cost of buy trade: 0.001002500 BTC
((90.99181073*0.00001099) + ((90.99181073*0.00001099)*0.0025))
Sell
- Sell: 90.99181073 Crypto at 0.00001173 BTC
(90.99181073*0.00001173 = 0,00106733394 BTC)
- Selling fee: 0.25%
- Total cost of sell trade: 0.001064666 BTC
((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025))
Profit/Loss: +0.000062166 BTC
(Sell:0.001064666 - Buy:0.001002500)
Profit/Loss percentage: 0.0620
((0.001064666/0.001002500)-1 = 6.20%)
:param limit_buy_order:
:param limit_sell_order:
:return:
"""
trade = Trade(
pair='BTC_ETH',
stake_amount=1.00,
fee=0.1,
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
assert trade.open_order_id is None
@@ -20,18 +131,53 @@ def test_update(limit_buy_order, limit_sell_order):
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.open_order_id is None
assert trade.open_rate == 0.07256061
assert trade.open_rate == 0.00001099
assert trade.close_profit is None
assert trade.close_date is None
trade.open_order_id = 'something'
trade.update(limit_sell_order)
assert trade.open_order_id is None
assert trade.open_rate == 0.07256061
assert trade.close_profit == 0.00546755
assert trade.close_rate == 0.00001173
assert trade.close_profit == 0.06201057
assert trade.close_date is not None
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.calc_open_trade_price() == 0.001002500
trade.update(limit_sell_order)
assert trade.calc_close_trade_price() == 0.0010646656
# Profit in BTC
assert trade.calc_profit() == 0.00006217
# Profit in percent
assert trade.calc_profit_percent() == 0.06201057
def test_calc_close_trade_price_exception(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.calc_close_trade_price() == 0.0
def test_update_open_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
@@ -64,3 +210,103 @@ def test_update_invalid_order(limit_buy_order):
limit_buy_order['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'):
trade.update(limit_buy_order)
def test_calc_open_trade_price(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'open_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get the open rate price with the standard fee rate
assert trade.calc_open_trade_price() == 0.001002500
# Get the open rate price with a custom fee rate
assert trade.calc_open_trade_price(fee=0.003) == 0.001003000
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'close_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get the close rate price with a custom close rate and a regular fee rate
assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318
# Get the close rate price with a custom close rate and a custom fee rate
assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704
# Test when we apply a Sell order, and ask price with a custom fee rate
trade.update(limit_sell_order)
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972
def test_calc_profit(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Custom closing rate and regular fee rate
# Higher than open rate
assert trade.calc_profit(rate=0.00001234) == 0.00011753
# Lower than open rate
assert trade.calc_profit(rate=0.00000123) == -0.00089086
# Custom closing rate and custom fee rate
# Higher than open rate
assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697
# Lower than open rate
assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092
# Only custom fee without sell order applied
with pytest.raises(TypeError):
trade.calc_profit(fee=0.003)
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(limit_sell_order)
assert trade.calc_profit() == 0.00006217
# Test with a custom fee rate on the close trade
assert trade.calc_profit(fee=0.003) == 0.00006163
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get percent of profit with a custom rate (Higher than open rate)
assert trade.calc_profit_percent(rate=0.00001234) == 0.1172387
# Get percent of profit with a custom rate (Lower than open rate)
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863827
# Only custom fee without sell order applied
with pytest.raises(TypeError):
trade.calc_profit_percent(fee=0.003)
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(limit_sell_order)
assert trade.calc_profit_percent() == 0.06201057
# Test with a custom fee rate on the close trade
assert trade.calc_profit_percent(fee=0.003) == 0.0614782

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

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

@@ -7,8 +7,13 @@ from os import path
from freqtrade import exchange
from freqtrade.exchange import Bittrex
PAIRS = ['BTC-OK', 'BTC-NEO', 'BTC-DASH', 'BTC-ETC', 'BTC-ETH', 'BTC-SNT']
TICKER_INTERVAL = 1 # ticker interval in minutes (currently implemented: 1 and 5)
PAIRS = [
'BTC_BCC', 'BTC_ETH', 'BTC_MER', 'BTC_POWR', 'BTC_ETC',
'BTC_OK', 'BTC_NEO', 'BTC_EMC2', 'BTC_DASH', 'BTC_LSK',
'BTC_LTC', 'BTC_XZC', 'BTC_OMG', 'BTC_STRAT', 'BTC_XRP',
'BTC_QTUM', 'BTC_WAVES', 'BTC_VTC', 'BTC_XLM', 'BTC_MCO'
]
TICKER_INTERVAL = 5 # ticker interval in minutes (currently implemented: 1 and 5)
OUTPUT_DIR = path.dirname(path.realpath(__file__))
# Init Bittrex exchange
@@ -16,8 +21,8 @@ exchange._API = Bittrex({'key': '', 'secret': ''})
for pair in PAIRS:
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
filename = path.join(OUTPUT_DIR, '{}-{}m.json'.format(
pair.lower(),
filename = path.join(OUTPUT_DIR, '{}-{}.json'.format(
pair,
TICKER_INTERVAL,
))
with open(filename, 'w') as fp:

View File

@@ -19,12 +19,12 @@
# limitations under the License.
#
import sys
import warnings
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
import warnings
import sys
from datetime import datetime, timedelta
from pandas.core.base import PandasObject
# =============================================

8
install_ta-lib.sh Executable file
View File

@@ -0,0 +1,8 @@
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
curl -O -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
tar zxvf ta-lib-0.4.0-src.tar.gz
cd ta-lib && ./configure && make && sudo make install && cd ..
else
echo "TA-lib already installed, skipping download and build."
cd ta-lib && sudo make install && cd ..
fi

View File

@@ -1,24 +1,25 @@
-e git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex
SQLAlchemy==1.1.14
python-telegram-bot==8.1.1
arrow==0.10.0
python-bittrex==0.2.2
SQLAlchemy==1.2.0
python-telegram-bot==9.0.0
arrow==0.12.0
cachetools==2.0.1
requests==2.18.4
urllib3==1.22
wrapt==1.10.11
pandas==0.20.3
scikit-learn==0.19.0
scipy==0.19.1
pandas==0.22.0
scikit-learn==0.19.1
scipy==1.0.0
jsonschema==2.6.0
numpy==1.13.3
TA-Lib==0.4.10
pytest==3.2.3
numpy==1.14.0
TA-Lib==0.4.15
pytest==3.3.2
pytest-mock==1.6.3
pytest-cov==2.5.1
hyperopt==0.1
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
networkx==1.11
tabulate==0.8.1
tabulate==0.8.2
pymarketcap==3.3.147
# Required for plotting data
#matplotlib==2.1.0

View File

@@ -1,22 +1,41 @@
#!/usr/bin/env python3
import sys
import argparse
import matplotlib # Install PYQT5 manually if you want to test this helper function
matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt
from freqtrade import exchange, analyze
from freqtrade.misc import common_args_parser
def plot_analyzed_dataframe(pair: str) -> None:
def plot_parse_args(args ):
parser = common_args_parser(description='Graph utility')
parser.add_argument(
'-p', '--pair',
help = 'What currency pair',
dest = 'pair',
default = 'BTC_ETH',
type = str,
)
return parser.parse_args(args)
def plot_analyzed_dataframe(args) -> None:
"""
Calls analyze() and plots the returned dataframe
:param pair: pair as str
:return: None
"""
pair = args.pair
# Init Bittrex to use public API
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
dataframe = analyze.analyze_ticker(pair)
ticker = exchange.get_ticker_history(pair)
dataframe = analyze.analyze_ticker(ticker)
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
# Two subplots sharing x axis
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
@@ -47,5 +66,5 @@ def plot_analyzed_dataframe(pair: str) -> None:
if __name__ == '__main__':
plot_analyzed_dataframe('BTC_ETH')
args = plot_parse_args(sys.argv[1:])
plot_analyzed_dataframe(args)

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python3
import multiprocessing
import os
import subprocess
PROC_COUNT = multiprocessing.cpu_count() - 1
DB_NAME = 'freqtrade_hyperopt'
WORK_DIR = os.path.join(
os.path.sep,
os.path.abspath(os.path.dirname(__file__)),
'..', '.hyperopt', 'worker'
)
if not os.path.exists(WORK_DIR):
os.makedirs(WORK_DIR)
# Spawn workers
command = [
'hyperopt-mongo-worker',
'--mongo=127.0.0.1:1234/{}'.format(DB_NAME),
'--poll-interval=0.1',
'--workdir={}'.format(WORK_DIR),
]
processes = [subprocess.Popen(command) for i in range(PROC_COUNT)]
# Join all workers
for proc in processes:
proc.wait()

21
scripts/start-mongodb.py Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
import os
import subprocess
DB_PATH = os.path.join(
os.path.sep,
os.path.abspath(os.path.dirname(__file__)),
'..', '.hyperopt', 'mongodb'
)
if not os.path.exists(DB_PATH):
os.makedirs(DB_PATH)
subprocess.Popen([
'mongod',
'--bind_ip=127.0.0.1',
'--port=1234',
'--nohttpinterface',
'--dbpath={}'.format(DB_PATH),
]).wait()

4
setup.cfg Normal file
View File

@@ -0,0 +1,4 @@
[flake8]
#ignore =
max-line-length = 100
max-complexity = 12

View File

@@ -35,9 +35,7 @@ setup(name='freqtrade',
'TA-Lib',
'tabulate',
'cachetools',
],
dependency_links=[
"git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex"
'pymarketcap',
],
include_package_data=True,
zip_safe=False,