Compare commits

..

751 Commits

Author SHA1 Message Date
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
gcarq
b115963a70 Merge branch 'release/0.14.2' 2017-11-16 00:40:44 +01:00
gcarq
2e953a937d version bump 2017-11-16 00:40:36 +01:00
gcarq
4e05691cab check if balance list is empty (fixes #105) 2017-11-16 00:01:47 +01:00
gcarq
b5f58724a0 get_ticker_history: check if result is set (fixes #103) 2017-11-15 23:16:54 +01:00
gcarq
b83309b55d reduce calls_per_second to 1 2017-11-15 23:16:39 +01:00
gcarq
e8101a6da5 default BaseVolume to 0.0 if null 2017-11-14 17:48:19 +01:00
gcarq
dd9cb008fb refresh whitelist based on wallet health (fixes #60)
Refreshs the whitelist in each iteration based on the wallet health,
disabled wallets will be removed from the whitelist automatically.
2017-11-13 21:34:47 +01:00
gcarq
81f7172c4a sanitize get_ticker_history (fixes #100) 2017-11-13 19:54:09 +01:00
Michael Egger
bab59fbacd Merge pull request #99 from gcarq/more_triggers2
Expanding hyperopt
2017-11-13 12:11:15 +01:00
Janne Sinivirta
0f0b10b6cc adjust search spaces 2017-11-13 07:28:56 +02:00
Janne Sinivirta
8e68c5358e clean up prints during hyperopt 2017-11-12 09:44:31 +02:00
Janne Sinivirta
660f01b514 add hilbert transform leadsine trigger 2017-11-12 09:13:54 +02:00
Janne Sinivirta
13537e3ce4 add short ema guard to hyperopt 2017-11-12 08:45:32 +02:00
Janne Sinivirta
2963a90008 add stochastics trigger 2017-11-12 08:38:52 +02:00
Janne Sinivirta
15b20b83fa optimize hyperopt objective function 2017-11-12 08:30:58 +02:00
gcarq
1c3c316e45 reduce calls_per_second 2017-11-11 21:29:35 +01:00
gcarq
517879382b Add argument for dynamic-whitelist handling
If --dynamic-whitelist is passed the whitelist in the config file
is ignored. It gets automatically refreshed every 30 minutes and
currently selects the 20 topmost BaseVolume markets
2017-11-11 19:20:53 +01:00
gcarq
bcd3340a80 implement get_market_summaries 2017-11-11 19:20:16 +01:00
gcarq
12ae1e111e use get_candles from python-bittrex 2017-11-11 17:14:55 +01:00
gcarq
d3b3370f23 Add configurable throttle mechanism 2017-11-11 16:47:19 +01:00
gcarq
8f817a3634 use TTLCache for get_ticker_history 2017-11-11 15:29:31 +01:00
Janne Sinivirta
cf79b15651 use discrete values for filters 2017-11-11 11:50:10 +02:00
Janne Sinivirta
a4284351e3 fix green_candle 2017-11-11 11:22:12 +02:00
Janne Sinivirta
906caf329b remove two unused or poorly performing indicators 2017-11-11 11:22:12 +02:00
Janne Sinivirta
3db13fae13 add green_candle guard 2017-11-11 11:22:12 +02:00
Janne Sinivirta
274972f7af make faststoch trigger use crossed_above helper 2017-11-11 11:22:11 +02:00
Janne Sinivirta
83fd27e031 add sar reversal as trigger 2017-11-11 11:22:11 +02:00
gcarq
3126dcfcea drop sleep_time and use python-bittrex request delay 2017-11-10 23:39:49 +01:00
Michael Egger
72aec6c320 Merge pull request #96 from gcarq/feature/add-argparse
add argparse and implement basic arguments
2017-11-10 18:04:03 +01:00
gcarq
b709ccbf53 enhance logging messages 2017-11-10 17:56:03 +01:00
gcarq
7e99b13742 add missing commands to README 2017-11-10 17:27:19 +01:00
gcarq
8b464033ff add missing commands to README 2017-11-10 17:26:52 +01:00
gcarq
93c525a8fa Merge branch 'master' into develop 2017-11-10 17:18:21 +01:00
gcarq
54b15c1556 update README 2017-11-10 17:17:51 +01:00
gcarq
029f32af63 Merge tag '0.14.1' into develop
0.14.1
2017-11-09 23:53:14 +01:00
gcarq
de13df6ede Merge branch 'release/0.14.1' 2017-11-09 23:53:10 +01:00
gcarq
0de211674d version bump 2017-11-09 23:52:34 +01:00
gcarq
f7a27c156c add /version command handler 2017-11-09 23:51:32 +01:00
gcarq
98f11fc7bb fix sqlite threading issue 2017-11-09 23:45:22 +01:00
gcarq
013e13e546 use tabulate for /count 2017-11-09 23:45:03 +01:00
gcarq
6ff26c561a move plot_dataframe to scripts/ folder 2017-11-09 22:29:23 +01:00
gcarq
c81358c291 remove MagicBot 2017-11-09 22:11:02 +01:00
gcarq
ed34d9f22f add tests for /forcesell all 2017-11-09 22:08:28 +01:00
gcarq
ee05561ef3 refactor forcesellall to /forcesell all 2017-11-09 22:07:51 +01:00
Eoin
69ae99406a add telegram handler for forcesellall 2017-11-09 21:52:08 +01:00
gcarq
0cfbb56b6c enhance and test pair validation 2017-11-09 21:47:47 +01:00
gcarq
8960373f1c Merge tag '0.14.0' into develop
0.14.0
2017-11-09 20:56:12 +01:00
gcarq
349a91bd92 Merge branch 'release/0.14.0' 2017-11-09 20:56:07 +01:00
gcarq
991b43b7e5 version bump 2017-11-09 20:55:45 +01:00
gcarq
a0fa6abcdc use in-memory db for dry_run 2017-11-09 20:26:52 +01:00
gcarq
86501b43c0 adjust message formatting 2017-11-09 20:25:17 +01:00
gcarq
80592970e9 add more tests 2017-11-09 20:02:41 +01:00
gcarq
567ed4ecda remove version pinning from setup.py 2017-11-09 00:33:22 +01:00
gcarq
fafbb0abfe update python-bittrex to 0.2.0 2017-11-09 00:31:53 +01:00
gcarq
0f1a36b8e9 force to python3 2017-11-08 23:39:29 +01:00
gcarq
31c03cdce1 fix linter issue 2017-11-08 22:44:32 +01:00
gcarq
e01c85bb3a add argparse and implement basic arguments 2017-11-08 22:43:47 +01:00
gcarq
a1b91ad1ea remove unneeded wrapper function 2017-11-08 21:17:51 +01:00
gcarq
6ce6018bb7 add more tests 2017-11-07 22:27:44 +01:00
gcarq
18eec0f4d4 catch BaseException in command_handler 2017-11-07 22:27:16 +01:00
gcarq
32327c45c2 set close_date on sell_order update 2017-11-07 22:26:44 +01:00
gcarq
ba485fe2b2 return state changes 2017-11-07 22:26:08 +01:00
gcarq
f8084b117e apply pylint recommendations 2017-11-07 20:13:36 +01:00
gcarq
abdddd5193 define common fixtures 2017-11-07 20:12:56 +01:00
gcarq
8eeb02e592 make ticker interval configurable 2017-11-07 18:59:47 +01:00
gcarq
8555271102 remove unneeded header from get_ticker_history 2017-11-07 18:49:16 +01:00
gcarq
d921bae75e set executable bit 2017-11-07 18:42:40 +01:00
gcarq
a1388ef296 add tick_interval to get_ticker_history as an optional parameter 2017-11-07 18:41:48 +01:00
gcarq
ddc7c94a1d Merge branch 'develop' of https://github.com/gcarq/freqtrade into develop 2017-11-07 18:40:56 +01:00
Michael Egger
e36444df27 Merge pull request #95 from gcarq/improve_backtests
Share pytest fixtures. Cache testfile loading.
2017-11-07 18:40:00 +01:00
Janne Sinivirta
0395c92260 move testdata file loading to pytest fixture 2017-11-07 19:24:51 +02:00
gcarq
f03395b90d force python3 via shebang 2017-11-07 17:54:44 +01:00
gcarq
20d5628786 catch broader RequestException instead ConnectionError 2017-11-07 17:45:13 +01:00
gcarq
57e089efd3 fix NoneType issue in status command handle 2017-11-07 17:39:57 +01:00
Janne Sinivirta
fbbde9de25 put shared fixtures to conftest.py 2017-11-07 17:29:00 +02:00
Samuel Husso
3d42b9fd75 Merge pull request #94 from gcarq/autopep
autoformat with autopep8
2017-11-06 19:41:57 +02:00
Janne Sinivirta
adfae9e75c autoformat with autopep8 2017-11-06 19:17:23 +02:00
gcarq
117dfbb563 fix wording 2017-11-06 18:15:33 +01:00
Michael Egger
e66dc8b027 Merge pull request #93 from gcarq/feature/interpreter-version-check
add interpreter version check
2017-11-06 17:23:53 +01:00
Michael Egger
ae0b49f532 Merge pull request #92 from gcarq/feature/rework-dry_run-mode
rework dry_run
2017-11-06 16:54:55 +01:00
gcarq
a37ea13fd1 catch RuntimeError earlier
This makes it possible to to restart the bot, if there are temporary
server issues.
2017-11-06 01:03:37 +01:00
gcarq
cc29126d61 make download_backtest_data.py platform independent 2017-11-06 00:16:24 +01:00
gcarq
810f2f9243 drop minimum_date from get_ticker_history 2017-11-06 00:06:59 +01:00
gcarq
60e651cb4c only return data['result'] from get_ticker_history 2017-11-05 23:47:59 +01:00
gcarq
472ce8566d enhance bittrex exception messages 2017-11-05 22:47:55 +01:00
gcarq
27ac15f298 add tabulate to setup.py 2017-11-05 20:54:41 +01:00
gcarq
d12dba16db simplify status command 2017-11-05 18:35:32 +01:00
Michael Egger
0f1d114c03 Merge pull request #86 from flightcom/feature/advanced-status-command
telegram command: advanced status
2017-11-05 18:13:25 +01:00
gcarq
3e7700e9ac add interpreter version check 2017-11-05 17:44:58 +01:00
Sébastien Moreau
60615c232c Merge branch 'develop' into feature/advanced-status-command 2017-11-05 10:34:17 -05:00
Sébastien Moreau
3884cfb809 Merge branch 'develop' into feature/advanced-status-command 2017-11-05 10:32:53 -05:00
Sebastien Moreau
caa6e22e53 Adds unit tests 2017-11-05 10:26:03 -05:00
gcarq
19f6ff330c adapt float precision asserts 2017-11-05 16:21:13 +01:00
gcarq
8fdd127f72 fix float precision rendering 2017-11-05 16:13:55 +01:00
gcarq
0a5eba64e2 do not remove order from dry_run order list 2017-11-05 16:13:20 +01:00
gcarq
b82c4444b2 apply correct typehint 2017-11-05 16:12:58 +01:00
gcarq
95a17b8f98 dry_run: remove mock value notice 2017-11-05 15:35:15 +01:00
gcarq
325f72fd91 dry_run: keep list of open orders 2017-11-05 15:21:16 +01:00
Janne Sinivirta
a237225683 Merge pull request #91 from gcarq/multiple_builds_travis
Parallel build in Travis
2017-11-05 15:21:20 +02:00
Janne Sinivirta
29b173f4e7 only run four evals of hyperopt, just to check it works 2017-11-05 09:28:42 +02:00
Janne Sinivirta
50a979161c run parallel test envs 2017-11-05 09:27:49 +02:00
gcarq
264d71e29e fix some pylint warnings 2017-11-04 18:55:41 +01:00
gcarq
a873688a44 backtesting: init Trade with Bittrex fee 2017-11-04 18:43:23 +01:00
Michael Egger
7cc8533b8e Merge pull request #89 from gcarq/feature/take-fees-into-account
take fees into account & sell amount equal to amount purchased
2017-11-03 21:47:46 +01:00
gcarq
04342acff1 fix typo 2017-11-03 21:37:20 +01:00
gcarq
c37df0e70d inform about mocked values with dry_run 2017-11-03 21:36:55 +01:00
gcarq
460dfa1031 fix percentage formating in execute_sell 2017-11-02 19:00:25 +01:00
gcarq
08a1d3ca1d pylint changes 2017-11-02 19:00:25 +01:00
gcarq
1daeed4a52 fix assert 2017-11-02 19:00:25 +01:00
gcarq
99724e2458 use Decimal for profit calculation 2017-11-02 19:00:25 +01:00
gcarq
cd18629433 add fee to sqlalchemy model and respecting it in calc_profit 2017-11-02 19:00:25 +01:00
gcarq
41510fdb32 add dry_run for get_balance 2017-11-02 19:00:25 +01:00
gcarq
9cb249610a adapt dry_run return values 2017-11-02 19:00:25 +01:00
gcarq
543857ddb2 set initial open_rate and amount in create_trade
This is mostly needed by dry_run
2017-11-02 19:00:25 +01:00
gcarq
1e5b0e8726 adapt tests 2017-11-02 19:00:25 +01:00
gcarq
0d0d822904 bump dburl to tradesv3 2017-11-02 19:00:25 +01:00
gcarq
9ff4a7b205 refactor _process to update trade state 2017-11-02 19:00:25 +01:00
gcarq
0e96197a94 don't spend the whole coin balance when selling 2017-11-02 19:00:25 +01:00
gcarq
9b9d0250f7 replace get_open_oders() with get_order() and add property for fee 2017-11-02 18:58:55 +01:00
gcarq
4a35676794 rename and exchange instance and mark it as private 2017-11-02 18:58:55 +01:00
gcarq
465c91b9a9 telegram.cleanup: fix NoneType issue when telegram is deactivated 2017-11-02 18:56:57 +01:00
Sebastien Moreau
60249af04c Removes long format + pylint fixes 2017-11-02 13:25:19 -04:00
gcarq
c3653dc417 Merge branch 'master' of https://github.com/gcarq/freqtrade into develop 2017-11-01 18:37:27 +01:00
gcarq
3d61095ba4 modify header font size 2017-11-01 18:36:22 +01:00
gcarq
7a0be94cde adapt README 2017-11-01 18:32:27 +01:00
gcarq
fad6427078 coverage: omit vendor folder 2017-11-01 01:43:15 +01:00
gcarq
4dfde7f9a2 Merge tag '0.13.0' into develop
0.13.0
2017-11-01 01:15:35 +01:00
gcarq
e2eceaa904 Merge branch 'release/0.13.0' 2017-11-01 01:15:31 +01:00
gcarq
f34af0ad67 version bump 2017-11-01 01:15:06 +01:00
gcarq
e07904d436 PEP8 linting 2017-10-31 00:36:35 +01:00
gcarq
26468bef83 balance: filter currencies with 0.0 balances 2017-10-31 00:29:22 +01:00
Michael Egger
ea1b1e11ea Merge pull request #88 from gcarq/reduce_memory_use
Reduce memory use in backtesting
2017-10-31 00:28:38 +01:00
Janne Sinivirta
e68e6c0a1a reuse mock in hyperopt also 2017-10-30 22:31:28 +02:00
Janne Sinivirta
7190226c84 reuse same mock for get_ticker_history, just change return_value 2017-10-30 22:06:09 +02:00
gcarq
6f2915e25e move qtpylib to vendor folder
This is necessary to distribute qtpylib with
freqtrade when you install it globally.
2017-10-30 20:41:36 +01:00
gcarq
6f7ac0720b add qtpylib to manifest 2017-10-30 20:24:58 +01:00
gcarq
b76554a487 add __init__ file for qtpylib 2017-10-30 20:23:19 +01:00
Janne Sinivirta
8da55c3742 move patching of arrow.utcnow to remove 500 unnecessary mock objects 2017-10-30 19:56:53 +02:00
Michael Egger
05111edd04 Merge pull request #87 from gcarq/more_triggers
More triggers and guards to hyperopt
2017-10-30 14:43:18 +01:00
Sebastien Moreau
361bdd20d3 Updates README 2017-10-29 20:55:14 -04:00
Sebastien Moreau
8bdace68f6 Adds options for /status command 2017-10-29 20:51:38 -04:00
Sebastien Moreau
0e1eb20781 Adds /count command
Adds /count command

Adds /count command
2017-10-29 18:47:42 -04:00
Michael Egger
4c2dea83c5 Merge pull request #84 from gcarq/telegram/show-balance
Telegram command: /show balance
2017-10-29 22:05:10 +01:00
Janne Sinivirta
d066817d0b removed below_sma and over_sma to wait for better implementation 2017-10-29 21:33:57 +02:00
Janne Sinivirta
a632121368 add macd cross signal trigger to hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
473d09b5cd add ema50 and ema100. add long uptrend ema guard to hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
893738d6f0 add MACD to analyze 2017-10-29 21:33:57 +02:00
Janne Sinivirta
22cfef7d36 add ema5 cross ema10 trigger to hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
e1bbe1d9a9 adjust indicator ranges in hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
ec981b415a add RSI to hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
57a17697a0 add RSI, MOM, EMA5 and EMA10 to analyze 2017-10-29 21:33:57 +02:00
Samuel Husso
f4fe09ffbf added get_balances as a abstract method to the interface baseclass 2017-10-29 17:57:57 +02:00
Michael Egger
871b5e17ee Merge pull request #85 from gcarq/datetime_fixes
Performance improvements for backtesting
2017-10-29 15:56:20 +01:00
Janne Sinivirta
9b00fc3474 use .ix instead of .loc for small perf boost 2017-10-29 16:28:55 +02:00
Janne Sinivirta
3b1dc36d8a switch to using itertuples instead of iterrows as it's a lot faster 2017-10-29 16:28:55 +02:00
Janne Sinivirta
4edf8f2079 copy only needed columns before iterating over them 2017-10-29 16:28:55 +02:00
Janne Sinivirta
54987fd9ca do date parsing while loading json, not later 2017-10-29 16:28:55 +02:00
Janne Sinivirta
da9c3e7d7d remove leftover dates from removing date filtering 2017-10-29 16:28:55 +02:00
Michael Egger
a948142ef5 Merge pull request #83 from gcarq/better-hyperopt-objective
Better hyperopt objective
2017-10-29 14:13:44 +01:00
Samuel Husso
4f6c3f94e0 added tests to /balance, minor cleanup 2017-10-29 10:10:00 +02:00
Janne Sinivirta
25d6d6bbe5 remove unused imports from test_hyperopt 2017-10-28 15:32:29 +03:00
Janne Sinivirta
649781d823 store result strings, display best result in summary. switch to a lot better objective algo 2017-10-28 15:26:22 +03:00
Janne Sinivirta
08ca7a8166 change print to format so result can be used in hyperopt Trials 2017-10-28 15:26:22 +03:00
Samuel Husso
dd78c62c3d added new command to return balance across all currencies 2017-10-28 08:59:43 +03:00
Samuel Husso
29de1645fe Merge pull request #82 from gcarq/feature/handle-process-signals
handle SIGINT, SIGTERM and SIGABRT process signals
2017-10-28 08:49:42 +03:00
gcarq
4139b0b0c7 add signal handler for SIGINT, SIGTERM and SIGABRT 2017-10-27 15:52:14 +02:00
Samuel Husso
0c33e917d5 Merge pull request #79 from gcarq/qtpylib
Include new indicators from qtpylib
2017-10-27 12:11:04 +03:00
Janne Sinivirta
e401a016f5 change analyze tests to use full json dump from bittrex 2017-10-26 16:50:31 +03:00
Janne Sinivirta
e0fde8665c Merge pull request #80 from gcarq/fix-testdate-dl-path
download testdata to correct folder when running from project root
2017-10-26 10:37:38 +03:00
Samuel Husso
752520c823 When running from project root download the files to the testdata folder instead of cwd 2017-10-26 10:24:22 +03:00
Janne Sinivirta
6ba2492360 add Awesome Oscillator and try it in hyperopt 2017-10-25 18:37:20 +03:00
Janne Sinivirta
d5d798f6fa pull in new indicators from QTPYLib 2017-10-25 18:37:20 +03:00
Janne Sinivirta
9c9cf76a0d Merge pull request #78 from gcarq/refactor-backtest
Refactor backtest functionality
2017-10-25 18:19:44 +03:00
Samuel Husso
041e201713 remove duplicated backtesting from hyperopt 2017-10-25 08:17:17 +03:00
gcarq
e09505b22d Merge tag '0.12.0' into develop
0.12.0
2017-10-24 18:14:41 +02:00
gcarq
6b15cb9b10 Merge branch 'release/0.12.0' 2017-10-24 18:14:37 +02:00
gcarq
ff4fcdc760 version bump 2017-10-24 18:14:31 +02:00
Samuel Husso
f43ba44b15 refactor backtesting to its own method as we use it also in hyperopt 2017-10-24 07:58:42 +03:00
Michael Egger
79c3e0583d Merge pull request #76 from gcarq/hyperopt
Use hyperopt to find optimal parameters for buy strategy
2017-10-23 09:40:13 +02:00
Janne Sinivirta
f6ef8383bb remove filtering from analyze that is super slow and not really needed 2017-10-22 21:50:07 +03:00
Janne Sinivirta
6f5307fda7 use more aggressive stop loss for hyperopt 2017-10-22 17:15:57 +03:00
Janne Sinivirta
37004e331a remove unused import and commented out code 2017-10-22 17:14:55 +03:00
Janne Sinivirta
57acf85b42 try a different objective function 2017-10-22 17:11:01 +03:00
Michael Egger
96790d50e5 Merge pull request #77 from gcarq/help-command
Help command to Telegram bot
2017-10-21 13:51:08 +02:00
Janne Sinivirta
d32ff3410c add help command to telegram bot 2017-10-21 11:08:08 +03:00
Janne Sinivirta
35838f5e64 upgrade to latest telegram lib 2017-10-21 11:07:29 +03:00
Janne Sinivirta
913488910c bump minimum evaluations to 40 rounds 2017-10-21 10:29:05 +03:00
Janne Sinivirta
17b984a7cd adjust objective function to emphasize trade lenghts more 2017-10-21 10:28:43 +03:00
Janne Sinivirta
f79b44eefe adjust ROI map for shorter trades 2017-10-21 10:28:02 +03:00
Janne Sinivirta
146c254c0f start adding other triggers than just the lower BBands 2017-10-21 10:26:38 +03:00
Janne Sinivirta
ce2966dd7f add uptrend_sma to hyperopt 2017-10-20 18:29:38 +03:00
Janne Sinivirta
0fbca8b8ef add CCI to hyperopt 2017-10-20 13:14:28 +03:00
Janne Sinivirta
3f7a583de6 add SAR to hyperopt. add over/under sma options to hyperopt 2017-10-20 12:56:44 +03:00
Janne Sinivirta
1196983d5f change objective to emphasize shorter trades and include average profit 2017-10-20 10:39:36 +03:00
Janne Sinivirta
bbb2c7cf62 more parametrizing. adjust ranges for previous parameters 2017-10-20 10:39:04 +03:00
Janne Sinivirta
ff100bdac4 the optimizer expects values in the order of smaller is better 2017-10-19 18:29:57 +03:00
Janne Sinivirta
4feb038d0a add hyperopt dependencies 2017-10-19 17:46:41 +03:00
Janne Sinivirta
1792e0fb9b use hyperopt to find optimal parameter values for indicators 2017-10-19 17:12:49 +03:00
Janne Sinivirta
d4f8b3ebbc remove setup.cfg as it's not used but it messes with running a single test 2017-10-19 17:12:08 +03:00
Michael Egger
aeef9bac33 Merge pull request #70 from dertione/patch-2
Download automatically altcoin datas
2017-10-17 13:36:33 +02:00
Michael Egger
eff361a104 Merge pull request #73 from gcarq/small_tweaks_to_strategy
Small tweaks to strategy
2017-10-15 18:08:18 +02:00
dertione
389f9b45bb update pylint 10/10 2017-10-15 17:24:49 +02:00
Janne Sinivirta
c9741cb291 adjust roi settings for faster trades 2017-10-15 17:32:07 +03:00
Janne Sinivirta
bf6f563df2 small tweaks to buy strategy and it's visualization 2017-10-15 17:32:07 +03:00
Michael Egger
58f34d4f4b Merge pull request #71 from steerio/develop
More efficient and flexible Docker builds
2017-10-15 15:46:39 +02:00
Janne Sinivirta
2c4d0144ba Add note about binding sqlite with dry_run enabled 2017-10-15 14:40:02 +03:00
dertione
afd1a0bf9b update for pylint 2017-10-14 14:40:26 +02:00
dertione
37f6c213f6 fork test 2017-10-13 15:50:50 +02:00
Roland Venesz
76736902c6 Merge branch 'master' into develop 2017-10-13 15:48:25 +02:00
Roland Venesz
d266171ed8 Docker improvements (faster and more secure builds) 2017-10-13 15:47:13 +02:00
Michael Egger
e7df373544 Merge pull request #67 from gcarq/upgrade-deps
Upgrade dependencies
2017-10-12 09:49:45 +02:00
Michael Egger
aa4b64d0bb Merge pull request #65 from xsmile/patch-4
set exchange in analyze.__main__ to fix plotting
2017-10-12 09:42:20 +02:00
Michael Egger
4559ddd74f Merge pull request #64 from xsmile/patch-1
Bittrex provider
2017-10-12 09:37:15 +02:00
xsmile
eecc45f8ba set exchange in analyze.__main__ to fix plotting
requires #64
2017-10-11 20:04:31 +02:00
xsmile
d76476040a Bittrex provider
remove redundant 'name' property and pair validation call
2017-10-11 19:51:37 +02:00
Janne Sinivirta
0c8c149b86 Fix the command for running backtesting in README.md 2017-10-11 13:09:57 +03:00
Janne Sinivirta
60a7f8614c upgrade dependencies 2017-10-10 19:04:05 +03:00
gcarq
c31b67bf7a Merge tag '0.11.0' into develop
0.11.0
2017-10-10 17:55:10 +02:00
gcarq
604a888791 Merge branch 'release/0.11.0' 2017-10-10 17:55:05 +02:00
gcarq
bfac1936d9 version bump 2017-10-10 17:54:42 +02:00
Janne Sinivirta
b1de0de5a5 Merge pull request #61 from xsmile/patch-2
add exchange package to manifest
2017-10-09 10:30:41 +03:00
xsmile
75ea2c4e1a add exchange package to manifest 2017-10-08 23:01:36 +02:00
Michael Egger
5e0f143a38 Merge pull request #58 from xsmile/exchange-interface
Exchange refactoring
2017-10-08 15:56:50 +02:00
gcarq
2d983db2e0 Merge branch 'master' into develop 2017-10-08 15:15:44 +02:00
gcarq
d9b01eee15 adapt install section 2017-10-08 15:15:11 +02:00
xsmile
2e368ef7aa docstring fix 2017-10-07 18:10:45 +02:00
xsmile
34c774c067 move exchange module content to exchange package and the interface to a new module 2017-10-07 18:07:29 +02:00
xsmile
ac32850034 simplify exchange initialization 2017-10-07 17:38:33 +02:00
xsmile
95e5c2e6c1 remove 'enabled' property in exchange config 2017-10-07 17:36:48 +02:00
Janne Sinivirta
aef42336e6 fixes to README.md
- Fix the command for running unit tests
- Add command to run backtest unit tests
2017-10-06 14:12:23 +03:00
Janne Sinivirta
f78427d236 Merge pull request #57 from shusso/fix-backtest-path
fix incorrect backtest testdata path
2017-10-06 14:04:01 +03:00
xsmile
b9eb266236 Exchange refactoring 2017-10-06 12:22:04 +02:00
Samuel Husso
e0896fdd7b fix incorrect backtest testdata path 2017-10-06 10:54:04 +03:00
Michael Egger
11f97ccf87 Merge pull request #54 from gcarq/fix-coverage
Fix coverage
2017-10-02 19:29:33 +02:00
Janne Sinivirta
3506e3ceec try directly invoking pytest for fixing coveralls issue 2017-10-02 20:17:14 +03:00
Janne Sinivirta
27b2624a67 let pytest do coverage 2017-10-02 19:48:47 +03:00
Janne Sinivirta
8500032bff add coverage config file to omit test files from coverage report 2017-10-02 19:27:18 +03:00
Janne Sinivirta
b2522b8dbc add pytest-cov dependency 2017-10-02 19:17:54 +03:00
Janne Sinivirta
0f3ceebcd4 Merge pull request #53 from gcarq/feature/patch-missing-calls
patch missing calls
2017-10-02 10:38:01 +03:00
gcarq
f44ab2f44b patch missing http calls 2017-10-01 23:28:09 +02:00
Janne Sinivirta
3fe5302db3 Merge pull request #52 from gcarq/convert-to-pytest
Switch to using Pytest
2017-10-01 17:28:23 +03:00
Janne Sinivirta
ea62c49c3a fix passing parameters to pytest 2017-10-01 17:19:14 +03:00
Janne Sinivirta
02673b94dd use explicit package name for pytest running 2017-10-01 17:04:38 +03:00
Janne Sinivirta
17e8bbacc3 add pytest-mock to setup.py 2017-10-01 16:17:27 +03:00
Janne Sinivirta
463123adc5 Merge branch 'develop' into convert-to-pytest 2017-10-01 16:14:50 +03:00
Janne Sinivirta
5537f0bf5b simplify unnecessary == True and == False assertions 2017-10-01 15:45:31 +03:00
Janne Sinivirta
5551c9ec3b add pragmas to disable pylint warnings for missing docstrings in test files 2017-10-01 15:40:40 +03:00
Janne Sinivirta
ff145b6306 use mocker for mocking to get rid of deep nesting 2017-10-01 15:40:12 +03:00
Janne Sinivirta
add6c875d6 add pytest-mock to requirements.txt 2017-10-01 15:24:27 +03:00
gcarq
378b5a3b14 fix indentation 2017-10-01 14:07:09 +02:00
gcarq
e14cc2e4f6 add setup.cfg to force pytest 2017-10-01 14:06:18 +02:00
Janne Sinivirta
616d5b61cc remove numbers from test method names 2017-10-01 11:11:20 +03:00
Janne Sinivirta
9cca42e371 add pytest to requirements.txt 2017-10-01 11:06:40 +03:00
Janne Sinivirta
06ad311aa3 remove Test classes and use pytest fixtures 2017-10-01 11:02:47 +03:00
gcarq
e42edd9de7 add required folders to MANIFEST 2017-09-30 21:01:23 +02:00
gcarq
3456ead839 add numpy as required dep 2017-09-30 21:00:53 +02:00
gcarq
a4a1f7961a set executable bit 2017-09-30 21:00:42 +02:00
gcarq
8057333501 adapt Dockerfile for new project structure 2017-09-30 21:00:14 +02:00
gcarq
1eee0c91bf adapt README 2017-09-30 20:59:54 +02:00
Janne Sinivirta
53b4c3722e convert asserts to pytest style 2017-09-30 20:38:19 +03:00
gcarq
3f6f502e66 add code coverage badge 2017-09-30 19:05:37 +02:00
Janne Sinivirta
d73c656514 Merge pull request #50 from gcarq/feature/fix-whitelist-vanishing
fix whitelist vanishing
2017-09-30 20:00:38 +03:00
gcarq
f409bdbba8 add coveralls.io to measure code quality 2017-09-30 18:55:48 +02:00
gcarq
4b42e1af4b use deepcopy 2017-09-30 18:23:11 +02:00
gcarq
898ab5a370 implement test to reproduce it 2017-09-30 18:22:05 +02:00
Janne Sinivirta
6389778d49 Merge pull request #47 from gcarq/feature/project-structure
Refactor project structure (closes #34)
2017-09-30 19:19:00 +03:00
gcarq
b85b913657 revert coveralls 2017-09-30 17:13:08 +02:00
gcarq
bbef0edcd1 add coveralls.io to measure code quality 2017-09-30 17:06:15 +02:00
gcarq
8d3a6279b2 use pytest 2017-09-30 15:58:31 +02:00
gcarq
df4da75535 add pypi classifiers 2017-09-29 20:15:54 +02:00
gcarq
04bba626a8 define install_requires for package distribution 2017-09-29 20:07:50 +02:00
gcarq
9c6c21637d fix testsuite 2017-09-29 19:28:32 +02:00
gcarq
09b27d2094 add manifest file 2017-09-29 19:28:32 +02:00
gcarq
998a887736 add command line script 2017-09-29 19:28:32 +02:00
gcarq
00499fa0d7 add setup.py 2017-09-29 19:28:32 +02:00
gcarq
0c517ee3b6 move project into freqtrade/ 2017-09-29 19:28:32 +02:00
gcarq
b225b0cb90 remove python nightly interpreter 2017-09-29 19:07:25 +02:00
Janne Sinivirta
d045116297 upgraded to latest telegram library (8.0) 2017-09-29 18:59:05 +03:00
Janne Sinivirta
8b859ad358 Merge pull request #44 from gcarq/another-better-strategy
Better buy strategy and sell criteria
2017-09-29 13:32:00 +03:00
Janne Sinivirta
0085db825d Merge branch 'develop' into another-better-strategy 2017-09-29 13:13:44 +03:00
Janne Sinivirta
1f1e64560a adjust roi and stop loss in config.json.example 2017-09-29 09:58:00 +03:00
Janne Sinivirta
c9226a329c adjust roi and stop loss for backtesting 2017-09-29 09:56:52 +03:00
Janne Sinivirta
44cdf3e0c2 improved buy signal strategy 2017-09-29 09:55:11 +03:00
Janne Sinivirta
b97f0f0705 use btc-eth as default pair for analyze graph 2017-09-29 09:49:19 +03:00
Janne Sinivirta
b2f4778352 show last 24hours in analyze graph 2017-09-29 09:48:59 +03:00
Janne Sinivirta
272abed807 show two decimals in average profit in backtesting results 2017-09-29 09:46:45 +03:00
Michael Egger
0437546e4b Merge pull request #41 from xsmile/optional-telegram
Telegram: Fix being optional
2017-09-29 00:37:46 +02:00
xsmile
2df2041d53 Telegram: Fix being optional 2017-09-29 00:15:38 +02:00
Michael Egger
3b9d354a62 Merge pull request #30 from freshfunkee/feature/handle-empty-dataframe
dataframe empty check
2017-09-28 21:49:06 +02:00
Eoin
a45073997d review comments: change log to warning 2017-09-28 20:07:33 +01:00
gcarq
d102a8f8a1 Merge tag '0.10.0' into develop
0.10.0
2017-09-28 19:17:08 +02:00
gcarq
c20030783b Merge branch 'release/0.10.0' 2017-09-28 19:17:01 +02:00
gcarq
ff5a6633c6 version bump 2017-09-28 19:14:18 +02:00
gcarq
af6b07efb1 Merge branch 'master' of https://github.com/gcarq/freqtrade into develop 2017-09-28 19:03:53 +02:00
gcarq
d416aba95e add setup tutorial (closes #40) 2017-09-28 19:01:02 +02:00
gcarq
775414d494 add slack invite link 2017-09-28 19:00:42 +02:00
Michael Egger
f493df7a82 Merge pull request #33 from gcarq/disable-debuglog-backtest
Disable debug level logging when running backtesting
2017-09-28 16:52:55 +02:00
Janne Sinivirta
a2f7709cfd disable debug level logging when running backtesting 2017-09-28 17:00:14 +03:00
Janne Sinivirta
9a64522f45 Merge pull request #25 from alangvand/whitelist-on-sale
add whitelist to execute_sell and append sold pair to it
2017-09-28 11:02:23 +03:00
Janne Sinivirta
e7620b46ae Merge pull request #29 from gcarq/backtesting
Backtesting
2017-09-28 10:52:42 +03:00
Eoin
0e5edd08e5 add dataframe empty check 2017-09-27 23:43:32 +01:00
Janne Sinivirta
41849c4a1e add three more currency pairs to back testing. redownloaded all test data 2017-09-25 22:01:54 +03:00
Janne Sinivirta
c9dcc1e57c disable the backtesting by default 2017-09-25 21:39:43 +03:00
Janne Sinivirta
9f7a72a990 shorten report text 2017-09-25 21:39:28 +03:00
Janne Sinivirta
5f98649b7d backtesting over 7 different coins and a month of 5min ticker data 2017-09-25 21:06:15 +03:00
Janne Sinivirta
4198220b68 extract sell criteria to it's own method for testing 2017-09-25 21:05:37 +03:00
Janne Sinivirta
877dd6d3fa simplify sell conditions 2017-09-25 15:17:29 +03:00
Janne Sinivirta
f3ccca1c66 try running analyze_ticker with mock data 2017-09-24 17:23:29 +03:00
Janne Sinivirta
9b63f02e1c add set of test data 2017-09-24 17:20:25 +03:00
Janne Sinivirta
72432c1285 Fix link markup for issues 2017-09-21 09:08:13 -07:00
Michael Egger
be86a40207 Merge pull request #28 from gcarq/contribute-readme
Updated README.md
2017-09-21 15:30:43 +02:00
Janne Sinivirta
73ad3b4c85 Updated README.md 2017-09-20 08:06:10 -07:00
Janne Sinivirta
2bd51d8be3 Merge pull request #26 from gcarq/bid-balance
Set balance for bid price between ask and last
2017-09-20 07:58:20 -07:00
Janne Sinivirta
358a1eb73f add type hint for ticker 2017-09-20 07:34:47 -07:00
Janne Sinivirta
e5a742cf2e add a little more explanation for ask_last_balance to README 2017-09-18 05:09:45 -07:00
Janne Sinivirta
4d651e0082 add ask_last_balance to README.md 2017-09-18 05:08:05 -07:00
Janne Sinivirta
465dc47b23 balance bid price between ask and last 2017-09-17 23:21:46 +03:00
Janne Sinivirta
989682457e add a field to config for setting balance between trying to buy with ask price and last price 2017-09-17 22:37:46 +03:00
André Øien Langvand
e9e76da054 add whitelist to execute_sell and append sold pair to it 2017-09-17 18:42:42 +02:00
gcarq
f84c58c3eb add slack token 2017-09-12 19:20:49 +02:00
gcarq
d4fb7bd776 Merge branch 'develop' of https://github.com/gcarq/freqtrade into develop 2017-09-12 17:50:08 +02:00
Michael Egger
8ed8e1e103 Merge pull request #20 from vertti/newer-strategy
New buy strategy
2017-09-12 16:01:18 +02:00
Janne Sinivirta
1280670ea3 use five minute ticker for a much more stable indicators 2017-09-12 10:53:42 +02:00
Janne Sinivirta
cedc207097 remove unused import 2017-09-12 10:49:30 +02:00
Janne Sinivirta
a5b3428552 rename variable to get rid of bunch of pylint shadowing complaints 2017-09-12 10:49:10 +02:00
Janne Sinivirta
2221a0fbbc implement new buying strategy 2017-09-12 10:47:23 +02:00
gcarq
ffa32df40f remove poloniex from CONF_SCHEMA 2017-09-11 14:06:52 +02:00
gcarq
f91cd8ea96 drop support for poloniex 2017-09-11 13:59:38 +02:00
gcarq
48beb279c0 rename btc_amount to stake_amount 2017-09-11 13:59:11 +02:00
gcarq
dc1cfe7a7a Merge tag '0.9.0' into develop
0.9.0
2017-09-10 22:57:20 +02:00
102 changed files with 9635 additions and 1635 deletions

5
.coveragerc Normal file
View File

@@ -0,0 +1,5 @@
[run]
omit =
scripts/*
freqtrade/tests/*
freqtrade/vendor/*

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.git
.gitignore
Dockerfile
.dockerignore
config.json*
*.sqlite

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,2 +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,28 +1,37 @@
sudo: false
sudo: true
os:
- linux
- linux
language: python
python:
- 3.6
- nightly
matrix:
allow_failures:
- python: nightly
- 3.6
addons:
apt:
packages:
- libelf-dev
- libdw-dev
- binutils-dev
- libelf-dev
- 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 ..
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install -r requirements.txt
script:
- python -m unittest
- ./install_ta-lib.sh
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install --upgrade flake8 coveralls
- pip install -r requirements.txt
- 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

@@ -1,17 +1,23 @@
FROM python:3.6.2
FROM python:3.6.2
RUN pip install numpy
RUN apt-get update
RUN apt-get -y install build-essential
RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
RUN tar zxvf ta-lib-0.4.0-src.tar.gz
RUN cd ta-lib && ./configure && make && make install
# Install TA-lib
RUN apt-get update && apt-get -y install build-essential && apt-get clean
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
tar xzvf - && \
cd ta-lib && \
./configure && make && make install && \
cd .. && rm -rf ta-lib
ENV LD_LIBRARY_PATH /usr/local/lib
RUN mkdir -p /freqtrade
# Prepare environment
RUN mkdir /freqtrade
WORKDIR /freqtrade
ADD ./requirements.txt /freqtrade/requirements.txt
RUN pip install -r requirements.txt
ADD . /freqtrade
CMD python main.py
# Install dependencies
COPY requirements.txt /freqtrade/
RUN pip install -r requirements.txt
# Install and execute
COPY . /freqtrade/
RUN pip install -e .
ENTRYPOINT ["freqtrade"]

5
MANIFEST.in Normal file
View File

@@ -0,0 +1,5 @@
include LICENSE
include README.md
include config.json.example
recursive-include freqtrade *.py
include freqtrade/tests/testdata/*.json

235
README.md
View File

@@ -1,77 +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&service=github)](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
Simple High frequency trading bot for crypto currencies.
Currently supported exchanges: bittrex, poloniex (partly implemented)
This software is for educational purposes only.
Don't risk money which you are afraid to lose.
Simple High frequency trading bot for crypto currencies designed to
support multi exchanges and be controlled via Telegram.
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`
![freqtrade](https://raw.githubusercontent.com/gcarq/freqtrade/develop/docs/assets/freqtrade-screenshot.png)
Persistence is achieved through sqlite.
## 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.
#### Telegram RPC commands:
* /start: Starts the trader
* /stop: Stops the trader
* /status: Lists all open trades
* /profit: Lists cumulative profit from all finished trades
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
* /performance: Show performance of each finished trade grouped by pair
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.
#### 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:
We strongly recommend you to have coding and Python knowledge. Do not
hesitate to read the source code and understand the mechanism of this bot.
## Table of Contents
- [Features](#features)
- [Quick start](#quick-start)
- [Documentations](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
- [Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
- [Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
- [Strategy Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
- [Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
- [Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
- [Support](#support)
- [Help](#help--slack)
- [Bugs](#bugs--issues)
- [Feature Requests](#feature-requests)
- [Pull Requests](#pull-requests)
- [Basic Usage](#basic-usage)
- [Bot commands](#bot-commands)
- [Telegram RPC commands](#telegram-rpc-commands)
- [Requirements](#requirements)
- [Min hardware required](#min-hardware-required)
- [Software requirements](#software-requirements)
## Branches
The project is currently setup in two main branches:
- `develop` - This branch has often new features, but might also cause
breaking changes.
- `master` - This branch contains the latest stable release. The bot
'should' be stable on this branch, and is generally well tested.
## 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": {
"2880": 0.005, # Sell after 48 hours if there is at least 0.5% profit
"1440": 0.01, # Sell after 24 hours if there is at least 1% profit
"720": 0.02, # Sell after 12 hours if there is at least 2% profit
"360": 0.02, # Sell after 6 hours if there is at least 2% profit
"0": 0.025 # Sell immediately if there is at least 2.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).
The other values should be self-explanatory,
if not feel free to raise a github issue.
### [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.
#### Prerequisites
* python3.6
* sqlite
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
### [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.
#### Install
```
$ 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
$ ./main.py
### [Pull Requests](https://github.com/gcarq/freqtrade/pulls)
Feel like our bot is missing a feature? We welcome your pull requests!
Please read our
[Contributing document](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
to understand the requirements before sending your pull-requests.
**Important:** Always create your PR against the `develop` branch, not
`master`.
## Basic Usage
### Bot commands
```bash
usage: main.py [-h] [-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
--dynamic-whitelist [INT]
dynamically generate and update whitelist based on 24h
BaseVolume (Default 20 currencies)
--dry-run-db Force dry run to use a local DB
"tradesv3.dry_run.sqlite" instead of memory DB. Work
only if dry_run is enabled.
```
More details on:
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
- [How to use Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
- [How to use Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
### Telegram RPC commands
Telegram is not mandatory. However, this is a great way to control your
bot. More details on our
[documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
#### Execute tests
- `/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
```
$ python -m unittest
```
## Requirements
#### Docker
```
$ cd freqtrade
$ docker build -t freqtrade .
$ docker run --rm -it freqtrade
```
### 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
### 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

@@ -1,165 +0,0 @@
import time
from datetime import timedelta
import logging
import arrow
import requests
from pandas.io.json import json_normalize
from pandas import DataFrame
import talib.abstract as ta
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def get_ticker(pair: str, minimum_date: arrow.Arrow) -> dict:
"""
Request ticker data from Bittrex for a given currency pair
"""
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
}
params = {
'marketName': pair.replace('_', '-'),
'tickInterval': 'OneMin',
'_': minimum_date.timestamp * 1000
}
data = requests.get(url, params=params, headers=headers).json()
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return data
def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame:
"""
Analyses the trend for the given pair
:param pair: pair as str in format BTC_ETH or BTC-ETH
:return: DataFrame
"""
df = DataFrame(ticker) \
.drop('BV', 1) \
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \
.sort_values('date')
return df[df['date'].map(arrow.get) > minimum_date]
def populate_indicators(dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
"""
dataframe['close_30_ema'] = ta.EMA(dataframe, timeperiod=30)
dataframe['close_90_ema'] = ta.EMA(dataframe, timeperiod=90)
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.2)
# calculate StochRSI
stochrsi = ta.STOCHRSI(dataframe)
dataframe['stochrsi'] = stochrsi['fastd'] # values between 0-100, not 0-1
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macds'] = macd['macdsignal']
dataframe['macdh'] = macd['macdhist']
return dataframe
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy trend for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(dataframe['stochrsi'] < 20)
& (dataframe['macd'] > dataframe['macds'])
& (dataframe['close'] > dataframe['sar']),
'buy'
] = 1
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe
def analyze_ticker(pair: str) -> DataFrame:
"""
Get ticker data for given currency pair, push it to a DataFrame and
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
"""
minimum_date = arrow.utcnow().shift(hours=-6)
data = get_ticker(pair, minimum_date)
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe)
return dataframe
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)
latest = dataframe.iloc[-1]
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
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
def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
"""
Plots the given dataframe
:param dataframe: DataFrame
:param pair: pair as str
:return: None
"""
import matplotlib
matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt
# Three subplots sharing x axe
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
fig.suptitle(pair, fontsize=14, fontweight='bold')
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
ax1.plot(dataframe.index.values, dataframe['close_30_ema'], label='EMA(30)')
ax1.plot(dataframe.index.values, dataframe['close_90_ema'], label='EMA(90)')
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
ax1.legend()
ax2.plot(dataframe.index.values, dataframe['macd'], label='MACD')
ax2.plot(dataframe.index.values, dataframe['macds'], label='MACDS')
ax2.plot(dataframe.index.values, dataframe['macdh'], label='MACD Histogram')
ax2.plot(dataframe.index.values, [0] * len(dataframe.index.values))
ax2.legend()
ax3.plot(dataframe.index.values, dataframe['stochrsi'], label='StochRSI')
ax3.plot(dataframe.index.values, [80] * len(dataframe.index.values))
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
ax3.legend()
# Fine-tune figure; make subplots close to each other and hide x ticks for
# all but bottom plot.
fig.subplots_adjust(hspace=0)
plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
plt.show()
if __name__ == '__main__':
# Install PYQT5==5.9 manually if you want to test this helper function
while True:
pair = 'BTC_ANT'
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
# get_buy_signal(pair)
plot_dataframe(analyze_ticker(pair), pair)
time.sleep(60)

4
bin/freqtrade Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python3
from freqtrade.main import main
main()

View File

@@ -2,39 +2,50 @@
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"fiat_display_currency": "USD",
"dry_run": false,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.10,
"poloniex": {
"enabled": false,
"key": "key",
"secret": "secret",
"pair_whitelist": []
"unfilledtimeout": 600,
"bid_strategy": {
"ask_last_balance": 0.0
},
"bittrex": {
"enabled": true,
"exchange": {
"name": "bittrex",
"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",
"chat_id": "chat_id"
},
"initial_state": "running"
}
"initial_state": "running",
"internals": {
"process_throttle_secs": 5
}
}

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`

View File

@@ -1,201 +0,0 @@
import enum
import logging
from typing import List
from bittrex.bittrex import Bittrex
from poloniex import Poloniex
logger = logging.getLogger(__name__)
# Current selected exchange
EXCHANGE = None
_API = None
_CONF = {}
class Exchange(enum.Enum):
POLONIEX = 0
BITTREX = 1
def init(config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
global _API, EXCHANGE
_CONF.update(config)
if config['dry_run']:
logger.info('Instance is running with dry_run enabled')
use_poloniex = config.get('poloniex', {}).get('enabled', False)
use_bittrex = config.get('bittrex', {}).get('enabled', False)
if use_poloniex:
EXCHANGE = Exchange.POLONIEX
_API = Poloniex(key=config['poloniex']['key'], secret=config['poloniex']['secret'])
elif use_bittrex:
EXCHANGE = Exchange.BITTREX
_API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
else:
raise RuntimeError('No exchange specified. Aborting!')
# Check if all pairs are available
markets = get_markets()
for pair in config[EXCHANGE.name.lower()]['pair_whitelist']:
if pair not in markets:
raise RuntimeError('Pair {} is not available at Poloniex'.format(pair))
def buy(pair: str, rate: float, amount: float) -> str:
"""
Places a limit buy order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to purchase
:return: order_id of the placed buy order
"""
if _CONF['dry_run']:
return 'dry_run'
elif EXCHANGE == Exchange.POLONIEX:
_API.buy(pair, rate, amount)
# TODO: return order id
elif EXCHANGE == Exchange.BITTREX:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return data['result']['uuid']
def sell(pair: str, rate: float, amount: float) -> str:
"""
Places a limit sell order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to sell
:return: None
"""
if _CONF['dry_run']:
return 'dry_run'
elif EXCHANGE == Exchange.POLONIEX:
_API.sell(pair, rate, amount)
# TODO: return order id
elif EXCHANGE == Exchange.BITTREX:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return data['result']['uuid']
def get_balance(currency: str) -> float:
"""
Get account balance.
:param currency: currency as str, format: BTC
:return: float
"""
if _CONF['dry_run']:
return 999.9
elif EXCHANGE == Exchange.POLONIEX:
data = _API.returnBalances()
return float(data[currency])
elif EXCHANGE == Exchange.BITTREX:
data = _API.get_balance(currency)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return float(data['result']['Balance'] or 0.0)
def get_ticker(pair: str) -> dict:
"""
Get Ticker for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: dict
"""
if EXCHANGE == Exchange.POLONIEX:
data = _API.returnTicker()
return {
'bid': float(data[pair]['highestBid']),
'ask': float(data[pair]['lowestAsk']),
'last': float(data[pair]['last'])
}
elif EXCHANGE == Exchange.BITTREX:
data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return {
'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']),
}
def cancel_order(order_id: str) -> None:
"""
Cancel order for given order_id
:param order_id: id as str
:return: None
"""
if _CONF['dry_run']:
pass
elif EXCHANGE == Exchange.POLONIEX:
raise NotImplemented('Not implemented')
elif EXCHANGE == Exchange.BITTREX:
data = _API.cancel(order_id)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
def get_open_orders(pair: str) -> List[dict]:
"""
Get all open orders for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: list of dicts
"""
if _CONF['dry_run']:
return []
elif EXCHANGE == Exchange.POLONIEX:
raise NotImplemented('Not implemented')
elif EXCHANGE == Exchange.BITTREX:
data = _API.get_open_orders(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return [{
'id': entry['OrderUuid'],
'type': entry['OrderType'],
'opened': entry['Opened'],
'rate': entry['PricePerUnit'],
'amount': entry['Quantity'],
'remaining': entry['QuantityRemaining'],
} for entry in data['result']]
def get_pair_detail_url(pair: str) -> str:
"""
Returns the market detail url for the given pair
:param pair: pair as str, format: BTC_ANT
:return: url as str
"""
if EXCHANGE == Exchange.POLONIEX:
raise NotImplemented('Not implemented')
elif EXCHANGE == Exchange.BITTREX:
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
def get_markets() -> List[str]:
"""
Returns all available markets
:return: list of all available pairs
"""
if EXCHANGE == Exchange.POLONIEX:
# TODO: implement
raise NotImplemented('Not implemented')
elif EXCHANGE == Exchange. BITTREX:
data = _API.get_markets()
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']]

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

16
freqtrade/__init__.py Normal file
View File

@@ -0,0 +1,16 @@
""" FreqTrade bot """
__version__ = '0.14.3'
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.
"""

315
freqtrade/analyze.py Normal file
View File

@@ -0,0 +1,315 @@
"""
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
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
:param ticker: See exchange.get_ticker_history
:return: DataFrame
"""
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
frame = DataFrame(ticker) \
.drop('BV', 1) \
.rename(columns=columns)
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
frame.sort_values('date', inplace=True)
return frame
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.
"""
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
# Awesome oscillator
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
"""
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
dataframe['cci'] = ta.CCI(dataframe)
"""
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# Minus Directional Indicator / Movement
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
"""
# 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 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
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[
(
(
(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 False
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
if dataframe.empty:
return False
latest = dataframe.iloc[-1]
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
if signal_date < arrow.now() - timedelta(minutes=10):
return False
result = latest[signal.value] == 1
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
return result

View File

@@ -0,0 +1,185 @@
# 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
logger = logging.getLogger(__name__)
# Current selected exchange
_API: Exchange = None
_CONF: dict = {}
# Holds all open sell orders for dry_run
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
class Exchanges(enum.Enum):
"""
Maps supported exchange names to correspondent classes.
"""
BITTREX = Bittrex
def init(config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
global _CONF, _API
_CONF.update(config)
if config['dry_run']:
logger.info('Instance is running with dry_run enabled')
exchange_config = config['exchange']
# Find matching class for the given exchange name
name = exchange_config['name']
try:
exchange_class = Exchanges[name.upper()].value
except KeyError:
raise OperationalException('Exchange {} is not supported'.format(name))
_API = exchange_class(exchange_config)
# Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist'])
def validate_pairs(pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
Raises OperationalException if one pair is not available.
:param pairs: list of pairs
:return: None
"""
try:
markets = _API.get_markets()
except requests.exceptions.RequestException as e:
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return
stake_cur = _CONF['stake_currency']
for pair in pairs:
if not pair.startswith(stake_cur):
raise OperationalException(
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
)
if pair not in markets:
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, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
'amount': amount,
'type': 'LIMIT_BUY',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
}
return order_id
return _API.buy(pair, rate, amount)
def sell(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
'amount': amount,
'type': 'LIMIT_SELL',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
}
return order_id
return _API.sell(pair, rate, amount)
def get_balance(currency: str) -> float:
if _CONF['dry_run']:
return 999.9
return _API.get_balance(currency)
def get_balances():
if _CONF['dry_run']:
return []
return _API.get_balances()
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
return _API.get_ticker(pair, refresh)
@cached(TTLCache(maxsize=100, ttl=30))
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List[Dict]:
return _API.get_ticker_history(pair, tick_interval)
def cancel_order(order_id: str) -> None:
if _CONF['dry_run']:
return
return _API.cancel_order(order_id)
def get_order(order_id: str) -> Dict:
if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id]
order.update({
'id': order_id
})
return order
return _API.get_order(order_id)
def get_pair_detail_url(pair: str) -> str:
return _API.get_pair_detail_url(pair)
def get_markets() -> List[str]:
return _API.get_markets()
def get_market_summaries() -> List[Dict]:
return _API.get_market_summaries()
def get_name() -> str:
return _API.name
def get_fee() -> float:
return _API.fee
def get_wallet_health() -> List[Dict]:
return _API.get_wallet_health()

View File

@@ -0,0 +1,226 @@
import logging
import requests
from typing import Dict, List, Optional
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__)
_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):
"""
Bittrex API wrapper.
"""
# Base URL and API endpoints
BASE_URL: str = 'https://www.bittrex.com'
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
def __init__(self, config: dict) -> None:
global _API, _API_V2, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config)
_API = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V1_1,
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:
# 0.25 %: See https://bittrex.com/fees
return 0.0025
def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({currency})'.format(
message=data['message'],
currency=currency))
return float(data['result']['Balance'] or 0.0)
def get_balances(self):
data = _API.get_balances()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return data['result']
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self.cached_ticker.keys():
data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
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))
# Update the pair
self.cached_ticker[pair] = {
'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']),
}
return self.cached_ticker[pair]
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
if tick_interval == 1:
interval = 'oneMin'
elif tick_interval == 5:
interval = 'fiveMin'
else:
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
# These sanity check are necessary because bittrex cannot keep their API stable.
if not data.get('result'):
raise ContentDecodingError('{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():
raise ContentDecodingError('{message} params=({pair})'.format(
message='Required property {} not present in response'.format(prop),
pair=pair))
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
return data['result']
def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
data = data['result']
return {
'id': data['OrderUuid'],
'type': data['Type'],
'pair': data['Exchange'].replace('-', '_'),
'opened': data['Opened'],
'rate': data['PricePerUnit'],
'amount': data['Quantity'],
'remaining': data['QuantityRemaining'],
'closed': data['Closed'],
}
def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
def get_pair_detail_url(self, pair: str) -> str:
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
def get_markets(self) -> List[str]:
data = _API.get_markets()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{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']:
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']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return [{
'Currency': entry['Health']['Currency'],
'IsActive': entry['Health']['IsActive'],
'LastChecked': entry['Health']['LastChecked'],
'Notice': entry['Currency'].get('Notice'),
} for entry in data['result']]

View File

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

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

472
freqtrade/main.py Executable file
View File

@@ -0,0 +1,472 @@
#!/usr/bin/env python3
import copy
import json
import logging
import sys
import time
import traceback
from datetime import datetime
from typing import Dict, List, Optional
import arrow
import requests
from cachetools import cached, TTLCache
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
logger = logging.getLogger('freqtrade')
_CONF = {}
def refresh_whitelist(whitelist: List[str]) -> List[str]:
"""
Check wallet health and remove pair from whitelist if necessary
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to trade
:return: the list of pairs the user wants to trade without the one unavailable or black_listed
"""
sanitized_whitelist = whitelist
health = exchange.get_wallet_health()
known_pairs = set()
for status in health:
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
# 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
# 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'
)
# 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(nb_assets: Optional[int] = 0) -> bool:
"""
Queries the persistence layer for open trades and handles them,
otherwise a new trade is created.
: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
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
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 DependencyException as exception:
logger.warning('Unable to create trade: %s', exception)
for trade in trades:
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Got open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
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:
logger.warning(
'Got %s in _process(), retrying in 30 seconds...',
error
)
time.sleep(30)
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 OperationalException. Stopping trader ...')
update_state(State.STOPPED)
return state_changed
def check_handle_timedout(timeoutvalue: int) -> None:
"""
Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out
:return: None
"""
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
else:
# TODO: figure out how to handle partially complete sell orders
pass
def execute_sell(trade: Trade, limit: float) -> None:
"""
Executes a limit sell for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:return: None
"""
# Execute sell and update trade record
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
trade.open_order_id = order_id
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
)
# 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 min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
"""
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
:return True if bot should sell at current rate
"""
current_profit = trade.calc_profit_percent(current_rate)
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
logger.debug('Stop loss hit.')
return True
# 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%%)', float(current_profit) * 100.0)
return False
def handle_trade(trade: Trade) -> bool:
"""
Sells the current pair if the threshold is reached and updates the trade record.
:return: True if trade has been sold, False otherwise
"""
if not trade.is_open:
raise ValueError('attempt to handle closed trade: {}'.format(trade))
logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid']
# 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
def get_target_bid(ticker: Dict[str, float]) -> float:
""" Calculates bid target between current ask price and last price """
if ticker['ask'] < ticker['last']:
return ticker['ask']
balance = _CONF['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
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 ...',
stake_amount
)
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
)
# Remove currently opened and latest pairs from whitelist
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
if trade.pair in whitelist:
whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist:
raise DependencyException('No pair in whitelist')
# Pick pair based on StochRSI buy signals
for _pair in whitelist:
if get_signal(_pair, SignalType.BUY):
pair = _pair
break
else:
return False
# Calculate amount
buy_limit = get_target_bid(exchange.get_ticker(pair))
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
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '.format(
exchange.get_name().upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit, stake_amount, _CONF['stake_currency'],
stake_amount_fiat, _CONF['fiat_display_currency']
))
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
trade = Trade(
pair=pair,
stake_amount=stake_amount,
amount=amount,
fee=exchange.get_fee(),
open_rate=buy_limit,
open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
open_order_id=order_id
)
Trade.session.add(trade)
Trade.session.flush()
return True
def init(config: dict, db_url: Optional[str] = None) -> None:
"""
Initializes all modules and updates the config
:param config: config as dict
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
# Initialize all modules
rpc.init(config)
persistence.init(config, db_url)
exchange.init(config)
# Set initial application state
initial_state = config.get('initial_state')
if initial_state:
update_state(State[initial_state.upper()])
else:
update_state(State.STOPPED)
@cached(TTLCache(maxsize=1, ttl=1800))
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 key: sort key (defaults to 'BaseVolume')
:return: List of pairs
"""
summaries = sorted(
(s for s in exchange.get_market_summaries() if s['MarketName'].startswith(base_currency)),
key=lambda s: s.get(key) or 0.0,
reverse=True
)
return [s['MarketName'].replace('-', '_') for s in summaries]
def cleanup() -> None:
"""
Cleanup the application state und finish all pending tasks
:return: None
"""
rpc.send_msg('*Status:* `Stopping trader...`')
logger.info('Stopping trader and cleaning up modules...')
update_state(State.STOPPED)
persistence.cleanup()
rpc.cleanup()
exit(0)
def main(sysargv=sys.argv[1:]) -> None:
"""
Loads and validates the config and handles the main loop
:return: None
"""
global _CONF
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(
level=args.loglevel,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logger.info(
'Starting freqtrade %s (loglevel=%s)',
__version__,
logging.getLevelName(args.loglevel)
)
# Load and validate configuration
_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:
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logger.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
throttle(
_process,
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
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__':
main()

318
freqtrade/misc.py Normal file
View File

@@ -0,0 +1,318 @@
import argparse
import enum
import json
import logging
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__
logger = logging.getLogger(__name__)
class State(enum.Enum):
RUNNING = 0
STOPPED = 1
# Current application state
_STATE = State.STOPPED
@synchronized
def update_state(state: State) -> None:
"""
Updates the application state
:param state: new state
:return: None
"""
global _STATE
_STATE = state
@synchronized
def get_state() -> State:
"""
Gets the current application state
:return:
"""
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
takes at least `min_secs` to finish execution.
:param func: Any callable
:param min_secs: minimum execution time in seconds
:return: Any
"""
start = time.time()
result = func(*args, **kwargs)
end = time.time()
duration = max(min_secs - (end - start), 0.0)
logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
time.sleep(duration)
return result
def common_args_parser(description: str):
"""
Parses given common arguments and returns them as a parsed object.
"""
parser = argparse.ArgumentParser(
description=description
)
parser.add_argument(
'-v', '--verbose',
help='be verbose',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.INFO,
)
parser.add_argument(
'--version',
action='version',
version='%(prog)s {}'.format(__version__),
)
parser.add_argument(
'-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',
'properties': {
'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',
'patternProperties': {
'^[0-9.]+$': {'type': 'number'}
},
'minProperties': 1
},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
'bid_strategy': {
'type': 'object',
'properties': {
'ask_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False
},
},
'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': {
'enabled': {'type': 'boolean'},
'token': {'type': 'string'},
'chat_id': {'type': 'string'},
},
'required': ['enabled', 'token', 'chat_id']
},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': {
'type': 'object',
'properties': {
'process_throttle_secs': {'type': 'number'}
}
}
},
'definitions': {
'exchange': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'key': {'type': 'string'},
'secret': {'type': 'string'},
'pair_whitelist': {
'type': 'array',
'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
}
},
'required': ['name', 'key', 'secret', 'pair_whitelist']
}
},
'anyOf': [
{'required': ['exchange']}
],
'required': [
'max_open_trades',
'stake_currency',
'stake_amount',
'fiat_display_currency',
'dry_run',
'minimal_roi',
'bid_strategy',
'telegram'
]
}

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"
]
}
}

198
freqtrade/persistence.py Normal file
View File

@@ -0,0 +1,198 @@
import logging
from datetime import datetime
from decimal import Decimal, getcontext
from typing import Dict, Optional
import arrow
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
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool
logger = logging.getLogger(__name__)
_CONF = {}
_DECL_BASE = declarative_base()
def init(config: dict, engine: Optional[Engine] = None) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:param engine: database engine for sqlalchemy (Optional)
:return: None
"""
_CONF.update(config)
if not engine:
if _CONF.get('dry_run', False):
# the user wants dry run to use a DB
if _CONF.get('dry_run_db', False):
engine = create_engine('sqlite:///tradesv3.dry_run.sqlite')
# Otherwise dry run will store in memory
else:
engine = create_engine('sqlite://',
connect_args={'check_same_thread': False},
poolclass=StaticPool,
echo=False)
else:
engine = create_engine('sqlite:///tradesv3.sqlite')
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session()
Trade.query = session.query_property()
_DECL_BASE.metadata.create_all(engine)
def cleanup() -> None:
"""
Flushes all pending operations to disk.
:return: None
"""
Trade.session.flush()
class Trade(_DECL_BASE):
__tablename__ = 'trades'
id = Column(Integer, primary_key=True)
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True)
fee = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float)
close_rate = Column(Float)
close_profit = Column(Float)
stake_amount = Column(Float, nullable=False)
amount = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_order_id = Column(String)
def __repr__(self):
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
self.id,
self.pair,
self.amount,
self.open_rate,
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
)
def update(self, order: Dict) -> None:
"""
Updates this entity with amount and actual open/close rates.
:param order: order retrieved by exchange.get_order()
:return: None
"""
# Ignore open and cancelled orders
if not order['closed'] or order['rate'] is None:
return
logger.info('Updating trade (id=%d) ...', self.id)
getcontext().prec = 8 # Bittrex do not go above 8 decimal
if order['type'] == 'LIMIT_BUY':
# Update open rate and actual 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':
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_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).
If rate is not set self.close_rate will be used
:return: profit in percentage as float
"""
getcontext().prec = 8
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))

42
freqtrade/rpc/__init__.py Normal file
View File

@@ -0,0 +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)

624
freqtrade/rpc/telegram.py Normal file
View File

@@ -0,0 +1,624 @@
import logging
import re
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 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 __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
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__)
_UPDATER: Updater = None
_CONF = {}
_FIAT_CONVERT = CryptoToFiatConverter()
def init(config: dict) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:return: None
"""
global _UPDATER
_CONF.update(config)
if not is_enabled():
return
_UPDATER = Updater(token=config['telegram']['token'], workers=0)
# Register command handler and start telegram message polling
handles = [
CommandHandler('status', _status),
CommandHandler('profit', _profit),
CommandHandler('balance', _balance),
CommandHandler('start', _start),
CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance),
CommandHandler('daily', _daily),
CommandHandler('count', _count),
CommandHandler('help', _help),
CommandHandler('version', _version),
]
for handle in handles:
_UPDATER.dispatcher.add_handler(handle)
_UPDATER.start_polling(
clean=True,
bootstrap_retries=-1,
timeout=30,
read_latency=60,
)
logger.info(
'rpc.telegram is listening for following commands: %s',
[h.command for h in handles]
)
def cleanup() -> None:
"""
Stops all running telegram threads.
:return: None
"""
if not is_enabled():
return
_UPDATER.stop()
def is_enabled() -> bool:
"""
Returns True if the telegram module is activated, False otherwise
"""
return bool(_CONF['telegram'].get('enabled', False))
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
"""
Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler
:return: decorated function
"""
def wrapper(*args, **kwargs):
update = kwargs.get('update') or args[1]
# Reject unauthorized messages
chat_id = int(_CONF['telegram']['chat_id'])
if int(update.message.chat_id) != chat_id:
logger.info('Rejected unauthorized message from: %s', update.message.chat_id)
return wrapper
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
try:
return command_handler(*args, **kwargs)
except BaseException:
logger.exception('Exception occurred within Telegram module')
return wrapper
@authorized_only
def _status(bot: Bot, update: Update) -> None:
"""
Handler for /status.
Returns the current TradeThread status
:param bot: telegram bot
:param update: message update
:return: None
"""
# Check if additional parameters are passed
params = update.message.text.replace('/status', '').split(' ') \
if update.message.text else []
if 'table' in params:
_status_table(bot, update)
return
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades:
send_msg('*Status:* `no active trade`', bot=bot)
else:
for trade in trades:
order = 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, 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
message = """
*Trade ID:* `{trade_id}`
*Current Pair:* [{pair}]({market_url})
*Open Since:* `{date}`
*Amount:* `{amount}`
*Open Rate:* `{open_rate:.8f}`
*Close Rate:* `{close_rate}`
*Current Rate:* `{current_rate:.8f}`
*Close Profit:* `{close_profit}`
*Current Profit:* `{current_profit:.2f}%`
*Open Order:* `{open_order}`
""".format(
trade_id=trade.id,
pair=trade.pair,
market_url=exchange.get_pair_detail_url(trade.pair),
date=arrow.get(trade.open_date).humanize(),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit * 100, 2),
open_order='({} rem={:.8f})'.format(
order['type'], order['remaining']
) if order else None,
)
send_msg(message, bot=bot)
@authorized_only
def _status_table(bot: Bot, update: Update) -> None:
"""
Handler for /status table.
Returns the current TradeThread status in table format
:param bot: telegram bot
:param update: message update
:return: None
"""
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades:
send_msg('*Status:* `no active order`', bot=bot)
else:
trades_list = []
for trade in trades:
# calculate profit and send message to user
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_percent(current_rate))
])
columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0])
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
message = "<pre>{}</pre>".format(message)
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:
"""
Handler for /profit.
Returns a cumulative profit statistics.
:param bot: telegram bot
:param update: message update
:return: None
"""
trades = Trade.query.order_by(Trade.id).all()
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 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, False)['bid']
profit_percent = trade.calc_profit_percent(rate=current_rate)
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)) \
.group_by(Trade.pair) \
.order_by(text('profit_sum DESC')) \
.first()
if not best_pair:
send_msg('*Status:* `no closed trade`', bot=bot)
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:* 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(
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(),
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
best_pair=bp_pair,
best_rate=round(bp_rate * 100, 2),
)
send_msg(markdown_msg, bot=bot)
@authorized_only
def _balance(bot: Bot, update: Update) -> None:
"""
Handler for /balance
Returns current account balance per crypto
"""
output = ''
balances = [
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
output = '`All balances are zero.`'
for currency in balances:
output += """*Currency*: {Currency}
*Available*: {Available}
*Balance*: {Balance}
*Pending*: {Pending}
""".format(**currency)
send_msg(output)
@authorized_only
def _start(bot: Bot, update: Update) -> None:
"""
Handler for /start.
Starts TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() == State.RUNNING:
send_msg('*Status:* `already running`', bot=bot)
else:
update_state(State.RUNNING)
@authorized_only
def _stop(bot: Bot, update: Update) -> None:
"""
Handler for /stop.
Stops TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() == State.RUNNING:
send_msg('`Stopping trader ...`', bot=bot)
update_state(State.STOPPED)
else:
send_msg('*Status:* `already stopped`', bot=bot)
@authorized_only
def _forcesell(bot: Bot, update: Update) -> None:
"""
Handler for /forcesell <id>.
Sells the given trade at current price
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
trade_id = update.message.text.replace('/forcesell', '').strip()
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade)
return
# Query for trade
trade = Trade.query.filter(and_(
Trade.id == trade_id,
Trade.is_open.is_(True)
)).first()
if not trade:
send_msg('Invalid argument. See `/help` to view usage')
logger.warning('/forcesell: Invalid argument received')
return
_exec_forcesell(trade)
@authorized_only
def _performance(bot: Bot, update: Update) -> None:
"""
Handler for /performance.
Shows a performance statistic from finished trades
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
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}% ({count})</code>'.format(
index=i + 1,
pair=pair,
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)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _count(bot: Bot, update: Update) -> None:
"""
Handler for /count.
Returns the number of trades running
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
message = tabulate({
'current': [len(trades)],
'max': [_CONF['max_open_trades']]
}, headers=['current', 'max'], tablefmt='simple')
message = "<pre>{}</pre>".format(message)
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _help(bot: Bot, update: Update) -> None:
"""
Handler for /help.
Show commands of the bot
:param bot: telegram bot
:param update: message update
:return: None
"""
message = """
*/start:* `Starts the trader`
*/stop:* `Stops the trader`
*/status [table]:* `Lists all open trades`
*table :* `will display trades in a table`
*/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`
*/version:* `Show version`
"""
send_msg(message, bot=bot)
@authorized_only
def _version(bot: Bot, update: Update) -> None:
"""
Handler for /version.
Show version information
:param bot: telegram bot
:param update: message update
:return: None
"""
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def shorten_date(_date):
"""
Trim the date so it fits on small screens
"""
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)
new_date = re.sub('^an?', '1', new_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
:param msg: message
:param bot: alternative bot
:param parse_mode: telegram parse mode
:return: None
"""
if not is_enabled():
return
bot = bot or _UPDATER.bot
keyboard = [['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help']]
reply_markup = ReplyKeyboardMarkup(keyboard)
try:
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.',
network_err.message
)
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

218
freqtrade/tests/conftest.py Normal file
View File

@@ -0,0 +1,218 @@
# pragma pylint: disable=missing-docstring
from datetime import datetime
from unittest.mock import MagicMock
import arrow
import pytest
from jsonschema import validate
from telegram import Chat, Message, Update
from freqtrade.misc import CONF_SCHEMA
@pytest.fixture(scope="module")
def default_conf():
""" Returns validated configuration suitable for most tests """
configuration = {
"max_open_trades": 1,
"stake_currency": "BTC",
"stake_amount": 0.001,
"fiat_display_currency": "USD",
"dry_run": True,
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.10,
"unfilledtimeout": 600,
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"name": "bittrex",
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_TKN",
"BTC_TRST",
"BTC_SWT",
"BTC_BCC"
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "0"
},
"initial_state": "running"
}
validate(configuration, CONF_SCHEMA)
return configuration
@pytest.fixture
def update():
_update = Update(0)
_update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
return _update
@pytest.fixture
def ticker():
return MagicMock(return_value={
'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,
})
@pytest.fixture
def health():
return MagicMock(return_value=[{
'Currency': 'BTC',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'ETH',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'TRST',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'SWT',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'BCC',
'IsActive': False,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}])
@pytest.fixture
def limit_buy_order():
return {
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'pair': 'mocked',
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001099,
'amount': 90.99181073,
'remaining': 0.0,
'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,
}
@pytest.fixture
def limit_sell_order():
return {
'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL',
'pair': 'mocked',
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001173,
'amount': 90.99181073,
'remaining': 0.0,
'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

@@ -0,0 +1,668 @@
# 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
from sqlalchemy import create_engine
from telegram import Update, Message, Chat
from telegram.error import NetworkError
from freqtrade import __version__
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 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):
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
default_conf['telegram']['enabled'] = False
assert is_enabled() is False
def test_init_disabled(default_conf, mocker):
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
default_conf['telegram']['enabled'] = False
telegram.init(default_conf)
def test_authorized_only(default_conf, mocker):
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
chat = Chat(0, 0)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
state = {'called': False}
@authorized_only
def dummy_handler(*args, **kwargs) -> None:
state['called'] = True
dummy_handler(MagicMock(), update)
assert state['called'] is True
def test_authorized_only_unauthorized(default_conf, mocker):
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
chat = Chat(0xdeadbeef, 0)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
state = {'called': False}
@authorized_only
def dummy_handler(*args, **kwargs) -> None:
state['called'] = True
dummy_handler(MagicMock(), update)
assert state['called'] is False
def test_authorized_only_exception(default_conf, mocker):
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
@authorized_only
def dummy_handler(*args, **kwargs) -> None:
raise Exception('test')
dummy_handler(MagicMock(), update)
def test_status_handle(default_conf, update, ticker, 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)
init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED)
_status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
update_state(State.RUNNING)
_status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data
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 == 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_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,
buy=MagicMock(return_value='mocked_order_id'))
init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED)
_status_table(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
update_state(State.RUNNING)
_status_table(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data
create_trade(15.0)
_status_table(bot=MagicMock(), update=update)
text = re.sub('</?pre>', '', msg_mock.call_args_list[-1][0][0])
line = text.split("\n")
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
assert int(fields[0]) == 1
assert fields[1] == 'BTC_ETH'
assert msg_mock.call_count == 1
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_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://'))
_profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data
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 == 1
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock()
# 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
_profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
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, ticker_sell_up, 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)
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)
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.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_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
for _ in range(4):
create_trade(0.001)
rpc_mock.reset_mock()
update.message.text = '/forcesell all'
_forcesell(bot=MagicMock(), update=update)
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_signal', side_effect=lambda s, t: True)
msg_mock = 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())
init(default_conf, create_engine('sqlite://'))
# Trader is not running
update_state(State.STOPPED)
update.message.text = '/forcesell 1'
_forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0]
# No argument
msg_mock.reset_mock()
update_state(State.RUNNING)
update.message.text = '/forcesell'
_forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'Invalid argument' in msg_mock.call_args_list[0][0][0]
# Invalid argument
msg_mock.reset_mock()
update_state(State.RUNNING)
update.message.text = '/forcesell 123456'
_forcesell(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0]
def test_performance_handle(
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
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)
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
_performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>BTC_ETH\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
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_signal', side_effect=lambda s, t: True)
msg_mock = 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,
buy=MagicMock(return_value='mocked_order_id'))
init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED)
_count(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
update_state(State.RUNNING)
# Create some test data
create_trade(0.001)
msg_mock.reset_mock()
_count(bot=MagicMock(), update=update)
msg = '<pre> current max\n--------- -----\n 1 {}</pre>'.format(
default_conf['max_open_trades']
)
assert msg in msg_mock.call_args_list[0][0][0]
def test_performance_handle_invalid(default_conf, update, 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.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock())
init(default_conf, create_engine('sqlite://'))
# Trader is not running
update_state(State.STOPPED)
_performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0]
def test_start_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf,
init=MagicMock())
init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED)
assert get_state() == State.STOPPED
_start(bot=MagicMock(), update=update)
assert get_state() == State.RUNNING
assert msg_mock.call_count == 0
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.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf,
init=MagicMock())
init(default_conf, create_engine('sqlite://'))
update_state(State.RUNNING)
assert get_state() == State.RUNNING
_start(bot=MagicMock(), update=update)
assert get_state() == State.RUNNING
assert msg_mock.call_count == 1
assert 'already running' in msg_mock.call_args_list[0][0][0]
def test_stop_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf,
init=MagicMock())
init(default_conf, create_engine('sqlite://'))
update_state(State.RUNNING)
assert get_state() == State.RUNNING
_stop(bot=MagicMock(), update=update)
assert get_state() == State.STOPPED
assert msg_mock.call_count == 1
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
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.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf,
init=MagicMock())
init(default_conf, create_engine('sqlite://'))
update_state(State.STOPPED)
assert get_state() == State.STOPPED
_stop(bot=MagicMock(), update=update)
assert get_state() == State.STOPPED
assert msg_mock.call_count == 1
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
def test_balance_handle(default_conf, update, mocker):
mock_balance = [{
'Currency': 'BTC',
'Balance': 10.0,
'Available': 12.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
}, {
'Currency': 'ETH',
'Balance': 0.0,
'Available': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
}]
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
get_balances=MagicMock(return_value=mock_balance))
_balance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0]
assert 'Balance' in msg_mock.call_args_list[0][0][0]
def test_help_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
_help(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
def test_version_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
_version(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0]
def test_send_msg(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock())
bot = MagicMock()
send_msg('test', bot)
assert not bot.method_calls
bot.reset_mock()
default_conf['telegram']['enabled'] = True
send_msg('test', bot)
assert len(bot.method_calls) == 1
def test_send_msg_network_error(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
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'))
send_msg('test', bot)
# Bot should've tried to send it twice
assert len(bot.method_calls) == 2

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

@@ -0,0 +1,74 @@
# 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 (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-1.json') as data_file:
return parse_ticker_dataframe(json.load(data_file))
def test_dataframe_correct_columns(result):
assert result.columns.tolist() == \
['close', 'high', 'low', 'open', 'date', 'volume']
def test_dataframe_correct_length(result):
assert len(result.index) == 14395
def test_populates_buy_trend(result):
dataframe = populate_buy_trend(populate_indicators(result))
assert 'buy' 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):
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)
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

@@ -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

@@ -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

@@ -0,0 +1,676 @@
# 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 (_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_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.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'),
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 not trades
result = _process()
assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 1
trade = trades[0]
assert trade is not None
assert trade.stake_amount == default_conf['stake_amount']
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
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.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(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=requests.exceptions.RequestException))
init(default_conf, create_engine('sqlite://'))
result = _process()
assert result is False
assert sleep_mock.has_calls()
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.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=OperationalException))
init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING
result = _process()
assert result is False
assert get_state() == State.STOPPED
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.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,
get_wallet_health=health,
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 not trades
result = _process()
assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 1
result = _process()
assert result is False
def test_create_trade(default_conf, ticker, limit_buy_order, 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'))
# Save state of current whitelist
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
assert trade is not None
assert trade.stake_amount == 0.001
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
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_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(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_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'] = []
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_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.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://'))
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'
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
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_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'))
# Create trade and sell it
init(default_conf, create_engine('sqlite://'))
create_trade(0.001)
trade = Trade.query.first()
assert trade
trade.update(limit_buy_order)
trade.update(limit_sell_order)
assert trade.is_open is False
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
def test_balance_fully_last_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 10
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

@@ -0,0 +1,164 @@
# pragma pylint: disable=missing-docstring,C0103
import argparse
import json
import time
from copy import deepcopy
import pytest
from jsonschema import ValidationError
from freqtrade.misc import (common_args_parser, load_config, parse_args,
throttle)
def test_throttle():
def func():
return 42
start = time.time()
result = throttle(func, min_secs=0.1)
end = time.time()
assert result == 42
assert end - start > 0.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

@@ -0,0 +1,312 @@
# pragma pylint: disable=missing-docstring
import os
import pytest
from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade, init
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=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
assert trade.open_order_id is None
assert trade.open_rate is None
assert trade.close_profit is None
assert trade.close_date is None
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.open_order_id is None
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.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',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
)
assert trade.open_order_id is None
assert trade.open_rate is None
assert trade.close_profit is None
assert trade.close_date is None
limit_buy_order['closed'] = False
trade.update(limit_buy_order)
assert trade.open_order_id is None
assert trade.open_rate is None
assert trade.close_profit is None
assert trade.close_date is None
def test_update_invalid_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
)
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

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""This script generate json data from bittrex"""
import json
from os import path
from freqtrade import exchange
from freqtrade.exchange import Bittrex
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
exchange._API = Bittrex({'key': '', 'secret': ''})
for pair in PAIRS:
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
filename = path.join(OUTPUT_DIR, '{}-{}.json'.format(
pair,
TICKER_INTERVAL,
))
with open(filename, 'w') as fp:
json.dump(data, fp)

0
freqtrade/vendor/__init__.py vendored Normal file
View File

0
freqtrade/vendor/qtpylib/__init__.py vendored Normal file
View File

625
freqtrade/vendor/qtpylib/indicators.py vendored Normal file
View File

@@ -0,0 +1,625 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# QTPyLib: Quantitative Trading Python Library
# https://github.com/ranaroussi/qtpylib
#
# Copyright 2016 Ran Aroussi
#
# Licensed under the GNU Lesser General Public License, v3.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.gnu.org/licenses/lgpl-3.0.en.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import sys
import warnings
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
from pandas.core.base import PandasObject
# =============================================
# check min, python version
if sys.version_info < (3, 4):
raise SystemError("QTPyLib requires Python version >= 3.4")
# =============================================
warnings.simplefilter(action="ignore", category=RuntimeWarning)
# =============================================
def numpy_rolling_window(data, window):
shape = data.shape[:-1] + (data.shape[-1] - window + 1, window)
strides = data.strides + (data.strides[-1],)
return np.lib.stride_tricks.as_strided(data, shape=shape, strides=strides)
def numpy_rolling_series(func):
def func_wrapper(data, window, as_source=False):
series = data.values if isinstance(data, pd.Series) else data
new_series = np.empty(len(series)) * np.nan
calculated = func(series, window)
new_series[-len(calculated):] = calculated
if as_source and isinstance(data, pd.Series):
return pd.Series(index=data.index, data=new_series)
return new_series
return func_wrapper
@numpy_rolling_series
def numpy_rolling_mean(data, window, as_source=False):
return np.mean(numpy_rolling_window(data, window), -1)
@numpy_rolling_series
def numpy_rolling_std(data, window, as_source=False):
return np.std(numpy_rolling_window(data, window), -1)
# ---------------------------------------------
def session(df, start='17:00', end='16:00'):
""" remove previous globex day from df """
if len(df) == 0:
return df
# get start/end/now as decimals
int_start = list(map(int, start.split(':')))
int_start = (int_start[0] + int_start[1] - 1 / 100) - 0.0001
int_end = list(map(int, end.split(':')))
int_end = int_end[0] + int_end[1] / 100
int_now = (df[-1:].index.hour[0] + (df[:1].index.minute[0]) / 100)
# same-dat session?
is_same_day = int_end > int_start
# set pointers
curr = prev = df[-1:].index[0].strftime('%Y-%m-%d')
# globex/forex session
if not is_same_day:
prev = (datetime.strptime(curr, '%Y-%m-%d') -
timedelta(1)).strftime('%Y-%m-%d')
# slice
if int_now >= int_start:
df = df[df.index >= curr + ' ' + start]
else:
df = df[df.index >= prev + ' ' + start]
return df.copy()
# ---------------------------------------------
def heikinashi(bars):
bars = bars.copy()
bars['ha_close'] = (bars['open'] + bars['high'] +
bars['low'] + bars['close']) / 4
bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
bars.loc[:1, 'ha_open'] = bars['open'].values[0]
bars.loc[1:, 'ha_open'] = (
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
return pd.DataFrame(
index=bars.index,
data={
'open': bars['ha_open'],
'high': bars['ha_high'],
'low': bars['ha_low'],
'close': bars['ha_close']})
# ---------------------------------------------
def tdi(series, rsi_len=13, bollinger_len=34, rsi_smoothing=2,
rsi_signal_len=7, bollinger_std=1.6185):
rsi_series = rsi(series, rsi_len)
bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std)
signal = sma(rsi_series, rsi_signal_len)
rsi_series = sma(rsi_series, rsi_smoothing)
return pd.DataFrame(index=series.index, data={
"rsi": rsi_series,
"signal": signal,
"bbupper": bb_series['upper'],
"bblower": bb_series['lower'],
"bbmid": bb_series['mid']
})
# ---------------------------------------------
def awesome_oscillator(df, weighted=False, fast=5, slow=34):
midprice = (df['high'] + df['low']) / 2
if weighted:
ao = (midprice.ewm(fast).mean() - midprice.ewm(slow).mean()).values
else:
ao = numpy_rolling_mean(midprice, fast) - \
numpy_rolling_mean(midprice, slow)
return pd.Series(index=df.index, data=ao)
# ---------------------------------------------
def nans(len=1):
mtx = np.empty(len)
mtx[:] = np.nan
return mtx
# ---------------------------------------------
def typical_price(bars):
res = (bars['high'] + bars['low'] + bars['close']) / 3.
return pd.Series(index=bars.index, data=res)
# ---------------------------------------------
def mid_price(bars):
res = (bars['high'] + bars['low']) / 2.
return pd.Series(index=bars.index, data=res)
# ---------------------------------------------
def ibs(bars):
""" Internal bar strength """
res = np.round((bars['close'] - bars['low']) /
(bars['high'] - bars['low']), 2)
return pd.Series(index=bars.index, data=res)
# ---------------------------------------------
def true_range(bars):
return pd.DataFrame({
"hl": bars['high'] - bars['low'],
"hc": abs(bars['high'] - bars['close'].shift(1)),
"lc": abs(bars['low'] - bars['close'].shift(1))
}).max(axis=1)
# ---------------------------------------------
def atr(bars, window=14, exp=False):
tr = true_range(bars)
if exp:
res = rolling_weighted_mean(tr, window)
else:
res = rolling_mean(tr, window)
res = pd.Series(res)
return (res.shift(1) * (window - 1) + res) / window
# ---------------------------------------------
def crossed(series1, series2, direction=None):
if isinstance(series1, np.ndarray):
series1 = pd.Series(series1)
if isinstance(series2, int) or isinstance(series2, float) or isinstance(series2, np.ndarray):
series2 = pd.Series(index=series1.index, data=series2)
if direction is None or direction == "above":
above = pd.Series((series1 > series2) & (
series1.shift(1) <= series2.shift(1)))
if direction is None or direction == "below":
below = pd.Series((series1 < series2) & (
series1.shift(1) >= series2.shift(1)))
if direction is None:
return above or below
return above if direction is "above" else below
def crossed_above(series1, series2):
return crossed(series1, series2, "above")
def crossed_below(series1, series2):
return crossed(series1, series2, "below")
# ---------------------------------------------
def rolling_std(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
if min_periods == window:
return numpy_rolling_std(series, window, True)
else:
try:
return series.rolling(window=window, min_periods=min_periods).std()
except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
except BaseException:
return pd.rolling_std(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def rolling_mean(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
if min_periods == window:
return numpy_rolling_mean(series, window, True)
else:
try:
return series.rolling(window=window, min_periods=min_periods).mean()
except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
except BaseException:
return pd.rolling_mean(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def rolling_min(series, window=14, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
try:
return series.rolling(window=window, min_periods=min_periods).min()
except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except BaseException:
return pd.rolling_min(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def rolling_max(series, window=14, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
try:
return series.rolling(window=window, min_periods=min_periods).min()
except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except BaseException:
return pd.rolling_min(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def rolling_weighted_mean(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
return series.ewm(span=window, min_periods=min_periods).mean()
except BaseException:
return pd.ewma(series, span=window, min_periods=min_periods)
# ---------------------------------------------
def hull_moving_average(series, window=200):
wma = (2 * rolling_weighted_mean(series, window=window / 2)) - \
rolling_weighted_mean(series, window=window)
return rolling_weighted_mean(wma, window=np.sqrt(window))
# ---------------------------------------------
def sma(series, window=200, min_periods=None):
return rolling_mean(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def wma(series, window=200, min_periods=None):
return rolling_weighted_mean(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def hma(series, window=200):
return hull_moving_average(series, window=window)
# ---------------------------------------------
def vwap(bars):
"""
calculate vwap of entire time series
(input can be pandas series or numpy array)
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
"""
typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
volume = bars['volume'].values
return pd.Series(index=bars.index,
data=np.cumsum(volume * typical) / np.cumsum(volume))
# ---------------------------------------------
def rolling_vwap(bars, window=200, min_periods=None):
"""
calculate vwap using moving window
(input can be pandas series or numpy array)
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
"""
min_periods = window if min_periods is None else min_periods
typical = ((bars['high'] + bars['low'] + bars['close']) / 3)
volume = bars['volume']
left = (volume * typical).rolling(window=window,
min_periods=min_periods).sum()
right = volume.rolling(window=window, min_periods=min_periods).sum()
return pd.Series(index=bars.index, data=(left / right))
# ---------------------------------------------
def rsi(series, window=14):
"""
compute the n period relative strength indicator
"""
# 100-(100/relative_strength)
deltas = np.diff(series)
seed = deltas[:window + 1]
# default values
ups = seed[seed > 0].sum() / window
downs = -seed[seed < 0].sum() / window
rsival = np.zeros_like(series)
rsival[:window] = 100. - 100. / (1. + ups / downs)
# period values
for i in range(window, len(series)):
delta = deltas[i - 1]
if delta > 0:
upval = delta
downval = 0
else:
upval = 0
downval = -delta
ups = (ups * (window - 1) + upval) / window
downs = (downs * (window - 1.) + downval) / window
rsival[i] = 100. - 100. / (1. + ups / downs)
# return rsival
return pd.Series(index=series.index, data=rsival)
# ---------------------------------------------
def macd(series, fast=3, slow=10, smooth=16):
"""
compute the MACD (Moving Average Convergence/Divergence)
using a fast and slow exponential moving avg'
return value is emaslow, emafast, macd which are len(x) arrays
"""
macd = rolling_weighted_mean(series, window=fast) - \
rolling_weighted_mean(series, window=slow)
signal = rolling_weighted_mean(macd, window=smooth)
histogram = macd - signal
# return macd, signal, histogram
return pd.DataFrame(index=series.index, data={
'macd': macd.values,
'signal': signal.values,
'histogram': histogram.values
})
# ---------------------------------------------
def bollinger_bands(series, window=20, stds=2):
sma = rolling_mean(series, window=window)
std = rolling_std(series, window=window)
upper = sma + std * stds
lower = sma - std * stds
return pd.DataFrame(index=series.index, data={
'upper': upper,
'mid': sma,
'lower': lower
})
# ---------------------------------------------
def weighted_bollinger_bands(series, window=20, stds=2):
ema = rolling_weighted_mean(series, window=window)
std = rolling_std(series, window=window)
upper = ema + std * stds
lower = ema - std * stds
return pd.DataFrame(index=series.index, data={
'upper': upper.values,
'mid': ema.values,
'lower': lower.values
})
# ---------------------------------------------
def returns(series):
try:
res = (series / series.shift(1) -
1).replace([np.inf, -np.inf], float('NaN'))
except BaseException:
res = nans(len(series))
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def log_returns(series):
try:
res = np.log(series / series.shift(1)
).replace([np.inf, -np.inf], float('NaN'))
except BaseException:
res = nans(len(series))
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def implied_volatility(series, window=252):
try:
logret = np.log(series / series.shift(1)
).replace([np.inf, -np.inf], float('NaN'))
res = numpy_rolling_std(logret, window) * np.sqrt(window)
except BaseException:
res = nans(len(series))
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def keltner_channel(bars, window=14, atrs=2):
typical_mean = rolling_mean(typical_price(bars), window)
atrval = atr(bars, window) * atrs
upper = typical_mean + atrval
lower = typical_mean - atrval
return pd.DataFrame(index=bars.index, data={
'upper': upper.values,
'mid': typical_mean.values,
'lower': lower.values
})
# ---------------------------------------------
def roc(series, window=14):
"""
compute rate of change
"""
res = (series - series.shift(window)) / series.shift(window)
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def cci(series, window=14):
"""
compute commodity channel index
"""
price = typical_price(series)
typical_mean = rolling_mean(price, window)
res = (price - typical_mean) / (.015 * np.std(typical_mean))
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def stoch(df, window=14, d=3, k=3, fast=False):
"""
compute the n period relative strength indicator
http://excelta.blogspot.co.il/2013/09/stochastic-oscillator-technical.html
"""
highs_ma = pd.concat([df['high'].shift(i)
for i in np.arange(window)], 1).apply(list, 1)
highs_ma = highs_ma.T.max().T
lows_ma = pd.concat([df['low'].shift(i)
for i in np.arange(window)], 1).apply(list, 1)
lows_ma = lows_ma.T.min().T
fast_k = ((df['close'] - lows_ma) / (highs_ma - lows_ma)) * 100
fast_d = numpy_rolling_mean(fast_k, d)
if fast:
data = {
'k': fast_k,
'd': fast_d
}
else:
slow_k = numpy_rolling_mean(fast_k, k)
slow_d = numpy_rolling_mean(slow_k, d)
data = {
'k': slow_k,
'd': slow_d
}
return pd.DataFrame(index=df.index, data=data)
# ---------------------------------------------
def zscore(bars, window=20, stds=1, col='close'):
""" get zscore of price """
std = numpy_rolling_std(bars[col], window)
mean = numpy_rolling_mean(bars[col], window)
return (bars[col] - mean) / (std * stds)
# ---------------------------------------------
def pvt(bars):
""" Price Volume Trend """
pvt = ((bars['close'] - bars['close'].shift(1)) /
bars['close'].shift(1)) * bars['volume']
return pvt.cumsum()
# =============================================
PandasObject.session = session
PandasObject.atr = atr
PandasObject.bollinger_bands = bollinger_bands
PandasObject.cci = cci
PandasObject.crossed = crossed
PandasObject.crossed_above = crossed_above
PandasObject.crossed_below = crossed_below
PandasObject.heikinashi = heikinashi
PandasObject.hull_moving_average = hull_moving_average
PandasObject.ibs = ibs
PandasObject.implied_volatility = implied_volatility
PandasObject.keltner_channel = keltner_channel
PandasObject.log_returns = log_returns
PandasObject.macd = macd
PandasObject.returns = returns
PandasObject.roc = roc
PandasObject.rolling_max = rolling_max
PandasObject.rolling_min = rolling_min
PandasObject.rolling_mean = rolling_mean
PandasObject.rolling_std = rolling_std
PandasObject.rsi = rsi
PandasObject.stoch = stoch
PandasObject.zscore = zscore
PandasObject.pvt = pvt
PandasObject.tdi = tdi
PandasObject.true_range = true_range
PandasObject.mid_price = mid_price
PandasObject.typical_price = typical_price
PandasObject.vwap = vwap
PandasObject.rolling_vwap = rolling_vwap
PandasObject.weighted_bollinger_bands = weighted_bollinger_bands
PandasObject.rolling_weighted_mean = rolling_weighted_mean
PandasObject.sma = sma
PandasObject.wma = wma
PandasObject.hma = hma

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

257
main.py
View File

@@ -1,257 +0,0 @@
#!/usr/bin/env python
import json
import logging
import time
import traceback
from datetime import datetime
from typing import Optional
from jsonschema import validate
import exchange
import persistence
from persistence import Trade
from analyze import get_buy_signal
from misc import CONF_SCHEMA, get_state, State, update_state
from rpc import telegram
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
__author__ = "gcarq"
__copyright__ = "gcarq 2017"
__license__ = "GPLv3"
__version__ = "0.9.0"
_CONF = {}
def _process() -> None:
"""
Queries the persistence layer for open trades and handles them,
otherwise a new trade is created.
:return: None
"""
try:
# 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']), exchange.EXCHANGE)
if trade:
Trade.session.add(trade)
else:
logging.info('Got no buy signal...')
except ValueError:
logger.exception('Unable to create trade')
for trade in trades:
# Check if there is already an open order for this trade
orders = exchange.get_open_orders(trade.pair)
orders = [o for o in orders if o['id'] == trade.open_order_id]
if orders:
logger.info('There is an open order for: %s', orders[0])
else:
# Update state
trade.open_order_id = None
# Check if this trade can be closed
if not close_trade_if_fulfilled(trade):
# Check if we can sell our current pair
handle_trade(trade)
Trade.session.flush()
except (ConnectionError, json.JSONDecodeError) as error:
msg = 'Got {} in _process()'.format(error.__class__.__name__)
logger.exception(msg)
def close_trade_if_fulfilled(trade: Trade) -> bool:
"""
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
"""
# 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('No open orders found and trade is fulfilled. Marking %s as closed ...', trade)
return True
return False
def execute_sell(trade: Trade, current_rate: float) -> None:
"""
Executes a sell for the given trade and current rate
:param trade: Trade instance
:param current_rate: current rate
:return: None
"""
# Get available balance
currency = trade.pair.split('_')[1]
balance = exchange.get_balance(currency)
profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange.name,
trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair),
trade.close_rate,
round(profit, 2)
)
logger.info(message)
telegram.send_msg(message)
def handle_trade(trade: Trade) -> None:
"""
Sells the current pair if the threshold is reached and updates the trade record.
:return: None
"""
try:
if not trade.is_open:
raise ValueError('attempt to handle closed trade: {}'.format(trade))
logger.debug('Handling open trade %s ...', trade)
# Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid']
current_profit = 100.0 * ((current_rate - trade.open_rate) / trade.open_rate)
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']) * 100.0:
logger.debug('Stop loss hit.')
execute_sell(trade, current_rate)
return
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
duration, threshold = float(duration), float(threshold)
# Check if time matches and current rate is above threshold
time_diff = (datetime.utcnow() - trade.open_date).total_seconds() / 60
if time_diff > duration and current_rate > (1 + threshold) * trade.open_rate:
execute_sell(trade, current_rate)
return
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit)
except ValueError:
logger.exception('Unable to handle open order')
def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[Trade]:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend
:param _exchange: exchange to use
"""
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
whitelist = _CONF[_exchange.name.lower()]['pair_whitelist']
# Check if btc_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise ValueError(
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency'])
)
# Remove currently opened and latest pairs from whitelist
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
latest_trade = Trade.query.filter(Trade.is_open.is_(False)).order_by(Trade.id.desc()).first()
if latest_trade:
trades.append(latest_trade)
for trade in trades:
if trade.pair in whitelist:
whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist:
raise ValueError('No pair in whitelist')
# Pick pair based on StochRSI buy signals
for _pair in whitelist:
if get_buy_signal(_pair):
pair = _pair
break
else:
return None
open_rate = exchange.get_ticker(pair)['ask']
amount = stake_amount / open_rate
order_id = exchange.buy(pair, open_rate, amount)
# Create trade entity and return
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
_exchange.name,
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
open_rate
)
logger.info(message)
telegram.send_msg(message)
return Trade(pair=pair,
btc_amount=stake_amount,
open_rate=open_rate,
open_date=datetime.utcnow(),
amount=amount,
exchange=_exchange,
open_order_id=order_id,
is_open=True)
def init(config: dict, db_url: Optional[str] = None) -> None:
"""
Initializes all modules and updates the config
:param config: config as dict
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
# Initialize all modules
telegram.init(config)
persistence.init(config, db_url)
exchange.init(config)
# Set initial application state
initial_state = config.get('initial_state')
if initial_state:
update_state(State[initial_state.upper()])
else:
update_state(State.STOPPED)
def app(config: dict) -> None:
"""
Main function which handles the application state
:param config: config as dict
:return: None
"""
logger.info('Starting freqtrade %s', __version__)
init(config)
try:
old_state = get_state()
logger.info('Initial State: %s', old_state)
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
while True:
new_state = get_state()
# Log state transition
if new_state != old_state:
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logging.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
_process()
# We need to sleep here because otherwise we would run into bittrex rate limit
time.sleep(25)
old_state = new_state
except RuntimeError:
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
logger.exception('RuntimeError. Trader stopped!')
finally:
telegram.send_msg('*Status:* `Trader has stopped`')
if __name__ == '__main__':
with open('config.json') as file:
_CONF = json.load(file)
validate(_CONF, CONF_SCHEMA)
app(_CONF)

92
misc.py
View File

@@ -1,92 +0,0 @@
import enum
from wrapt import synchronized
class State(enum.Enum):
RUNNING = 0
STOPPED = 1
# Current application state
_STATE = State.STOPPED
@synchronized
def update_state(state: State) -> None:
"""
Updates the application state
:param state: new state
:return: None
"""
global _STATE
_STATE = state
@synchronized
def get_state() -> State:
"""
Gets the current application state
:return:
"""
return _STATE
# Required json-schema for user specified config
CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 1},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
'stake_amount': {'type': 'number', 'minimum': 0.0005},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
'patternProperties': {
'^[0-9.]+$': {'type': 'number'}
},
'minProperties': 1
},
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'poloniex': {'$ref': '#/definitions/exchange'},
'bittrex': {'$ref': '#/definitions/exchange'},
'telegram': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'token': {'type': 'string'},
'chat_id': {'type': 'string'},
},
'required': ['enabled', 'token', 'chat_id']
},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
},
'definitions': {
'exchange': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'key': {'type': 'string'},
'secret': {'type': 'string'},
'pair_whitelist': {
'type': 'array',
'items': {'type': 'string'},
'uniqueItems': True
}
},
'required': ['enabled', 'key', 'secret', 'pair_whitelist']
}
},
'anyOf': [
{'required': ['poloniex']},
{'required': ['bittrex']}
],
'required': [
'max_open_trades',
'stake_currency',
'stake_amount',
'dry_run',
'minimal_roi',
'telegram'
]
}

View File

@@ -1,89 +0,0 @@
from datetime import datetime
from typing import Optional
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.types import Enum
import exchange
_CONF = {}
Base = declarative_base()
def init(config: dict, db_url: Optional[str] = None) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
_CONF.update(config)
if not db_url:
if _CONF.get('dry_run', False):
db_url = 'sqlite:///tradesv2.dry_run.sqlite'
else:
db_url = 'sqlite:///tradesv2.sqlite'
engine = create_engine(db_url, echo=False)
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session()
Trade.query = session.query_property()
Base.metadata.create_all(engine)
class Trade(Base):
__tablename__ = 'trades'
id = Column(Integer, primary_key=True)
exchange = Column(Enum(exchange.Exchange), nullable=False)
pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True)
open_rate = Column(Float, nullable=False)
close_rate = Column(Float)
close_profit = Column(Float)
btc_amount = Column(Float, nullable=False)
amount = Column(Float, nullable=False)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime)
open_order_id = Column(String)
def __repr__(self):
if self.is_open:
open_since = 'closed'
else:
open_since = round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
self.id,
self.pair,
self.amount,
self.open_rate,
open_since
)
def exec_sell_order(self, rate: float, amount: float) -> float:
"""
Executes a sell for the given trade and updated the entity.
:param rate: rate to sell for
:param amount: amount to sell
:return: current profit as percentage
"""
profit = 100 * ((rate - self.open_rate) / self.open_rate)
# Execute sell and update trade record
order_id = exchange.sell(str(self.pair), rate, amount)
self.close_rate = rate
self.close_profit = profit
self.close_date = datetime.utcnow()
self.open_order_id = order_id
# Flush changes
Trade.session.flush()
return profit

View File

@@ -1,15 +1,26 @@
-e git+https://github.com/s4w3d0ff/python-poloniex.git#egg=Poloniex
-e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex
SQLAlchemy==1.1.13
python-telegram-bot==7.0.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
matplotlib==2.0.2
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
TA-Lib==0.4.10
#PYQT5==5.9
numpy==1.14.0
TA-Lib==0.4.14
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.2
pymarketcap==3.3.145
# Required for plotting data
#matplotlib==2.1.0
#PYQT5==5.9

View File

@@ -1 +0,0 @@
from . import telegram

View File

@@ -1,324 +0,0 @@
import logging
from datetime import timedelta
from typing import Callable, Any
import arrow
from sqlalchemy import and_, func, text
from telegram.error import NetworkError
from telegram.ext import CommandHandler, Updater
from telegram import ParseMode, Bot, Update
from misc import get_state, State, update_state
from persistence import Trade
import exchange
# Remove noisy log messages
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__)
_updater = None
_CONF = {}
def init(config: dict) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:return: None
"""
global _updater
_updater = Updater(token=config['telegram']['token'], workers=0)
_CONF.update(config)
# Register command handler and start telegram message polling
handles = [
CommandHandler('status', _status),
CommandHandler('profit', _profit),
CommandHandler('start', _start),
CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance),
]
for handle in handles:
_updater.dispatcher.add_handler(handle)
_updater.start_polling(
clean=True,
bootstrap_retries=3,
timeout=30,
read_latency=60,
)
logger.info(
'rpc.telegram is listening for following commands: %s',
[h.command for h in handles]
)
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
"""
Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler
:return: decorated function
"""
def wrapper(*args, **kwargs):
bot, update = kwargs.get('bot') or args[0], kwargs.get('update') or args[1]
if not isinstance(bot, Bot) or not isinstance(update, Update):
raise ValueError('Received invalid Arguments: {}'.format(*args))
chat_id = int(_CONF['telegram']['chat_id'])
if int(update.message.chat_id) == chat_id:
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
return command_handler(*args, **kwargs)
else:
logger.info('Rejected unauthorized message from: %s', update.message.chat_id)
return wrapper
@authorized_only
def _status(bot: Bot, update: Update) -> None:
"""
Handler for /status.
Returns the current TradeThread status
:param bot: telegram bot
:param update: message update
:return: None
"""
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades:
send_msg('*Status:* `no active order`', bot=bot)
else:
for trade in trades:
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair)['bid']
current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
orders = exchange.get_open_orders(trade.pair)
orders = [o for o in orders if o['id'] == trade.open_order_id]
order = orders[0] if orders else None
fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit, 2)
) if trade.close_profit else None
message = """
*Trade ID:* `{trade_id}`
*Current Pair:* [{pair}]({market_url})
*Open Since:* `{date}`
*Amount:* `{amount}`
*Open Rate:* `{open_rate}`
*Close Rate:* `{close_rate}`
*Current Rate:* `{current_rate}`
*Close Profit:* `{close_profit}`
*Current Profit:* `{current_profit:.2f}%`
*Open Order:* `{open_order}`
""".format(
trade_id=trade.id,
pair=trade.pair,
market_url=exchange.get_pair_detail_url(trade.pair),
date=arrow.get(trade.open_date).humanize(),
open_rate=trade.open_rate,
close_rate=trade.close_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit, 2),
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None,
)
send_msg(message, bot=bot)
@authorized_only
def _profit(bot: Bot, update: Update) -> None:
"""
Handler for /profit.
Returns a cumulative profit statistics.
:param bot: telegram bot
:param update: message update
:return: None
"""
trades = Trade.query.order_by(Trade.id).all()
profit_amounts = []
profits = []
durations = []
for trade in trades:
if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds())
if trade.close_profit:
profit = trade.close_profit
else:
# Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid']
profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
profit_amounts.append((profit / 100) * trade.btc_amount)
profits.append(profit)
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(text('profit_sum DESC')) \
.first()
if not best_pair:
send_msg('*Status:* `no closed trade`', bot=bot)
return
bp_pair, bp_rate = best_pair
markdown_msg = """
*ROI:* `{profit_btc:.2f} ({profit:.2f}%)`
*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), 2),
trade_count=len(trades),
first_trade_date=arrow.get(trades[0].open_date).humanize(),
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
best_pair=bp_pair,
best_rate=round(bp_rate, 2),
)
send_msg(markdown_msg, bot=bot)
@authorized_only
def _start(bot: Bot, update: Update) -> None:
"""
Handler for /start.
Starts TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() == State.RUNNING:
send_msg('*Status:* `already running`', bot=bot)
else:
update_state(State.RUNNING)
@authorized_only
def _stop(bot: Bot, update: Update) -> None:
"""
Handler for /stop.
Stops TradeThread
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() == State.RUNNING:
send_msg('`Stopping trader ...`', bot=bot)
update_state(State.STOPPED)
else:
send_msg('*Status:* `already stopped`', bot=bot)
@authorized_only
def _forcesell(bot: Bot, update: Update) -> None:
"""
Handler for /forcesell <id>.
Sells the given trade at current price
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
try:
trade_id = int(update.message.text
.replace('/forcesell', '')
.strip())
# Query for trade
trade = Trade.query.filter(and_(
Trade.id == trade_id,
Trade.is_open.is_(True)
)).first()
if not trade:
send_msg('There is no open trade with ID: `{}`'.format(trade_id))
return
# Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid']
# Get available balance
currency = trade.pair.split('_')[1]
balance = exchange.get_balance(currency)
# Execute sell
profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange.name,
trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair),
trade.close_rate,
round(profit, 2)
)
logger.info(message)
send_msg(message)
except ValueError:
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
logger.warning('/forcesell: Invalid argument received')
@authorized_only
def _performance(bot: Bot, update: Update) -> None:
"""
Handler for /performance.
Shows a performance statistic from finished trades
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
pair_rates = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
.filter(Trade.is_open.is_(False)) \
.group_by(Trade.pair) \
.order_by(text('profit_sum DESC')) \
.all()
stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format(
index=i + 1,
pair=pair,
profit=round(rate, 2)
) for i, (pair, rate) in enumerate(pair_rates))
message = '<b>Performance:</b>\n{}\n'.format(stats)
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML)
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message
:param msg: message
:param bot: alternative bot
:param parse_mode: telegram parse mode
:return: None
"""
if _CONF['telegram'].get('enabled', False):
try:
bot = bot or _updater.bot
try:
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
except NetworkError as error:
# 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
)
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
except Exception:
logger.exception('Exception occurred within Telegram API')

70
scripts/plot_dataframe.py Executable file
View File

@@ -0,0 +1,70 @@
#!/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_parse_args(args ):
parser = common_args_parser(args, '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': ''})
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)
fig.suptitle(pair, fontsize=14, fontweight='bold')
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')
ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA')
ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low')
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
ax1.legend()
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI')
# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
ax2.legend()
ax3.plot(dataframe.index.values, dataframe['fastk'], label='k')
ax3.plot(dataframe.index.values, dataframe['fastd'], label='d')
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
ax3.legend()
# Fine-tune figure; make subplots close to each other and hide x ticks for
# all but bottom plot.
fig.subplots_adjust(hspace=0)
plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
plt.show()
if __name__ == '__main__':
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

47
setup.py Normal file
View File

@@ -0,0 +1,47 @@
from sys import version_info
from setuptools import setup
if version_info.major == 3 and version_info.minor < 6 or \
version_info.major < 3:
print('Your Python interpreter must be 3.6 or greater!')
exit(1)
from freqtrade import __version__
setup(name='freqtrade',
version=__version__,
description='Simple High Frequency Trading Bot for crypto currencies',
url='https://github.com/gcarq/freqtrade',
author='gcarq and contributors',
author_email='michael.egger@tsn.at',
license='GPLv3',
packages=['freqtrade'],
scripts=['bin/freqtrade'],
setup_requires=['pytest-runner'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[
'python-bittrex',
'SQLAlchemy',
'python-telegram-bot',
'arrow',
'requests',
'urllib3',
'wrapt',
'pandas',
'scikit-learn',
'scipy',
'jsonschema',
'TA-Lib',
'tabulate',
'cachetools',
'pymarketcap',
],
include_package_data=True,
zip_safe=False,
classifiers=[
'Programming Language :: Python :: 3.6',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Topic :: Office/Business :: Financial :: Investment',
'Intended Audience :: Science/Research',
])

View File

@@ -1,49 +0,0 @@
# pragma pylint: disable=missing-docstring
import unittest
from unittest.mock import patch
from pandas import DataFrame
import arrow
from analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, analyze_ticker, get_buy_signal
RESULT_BITTREX = {
'success': True,
'message': '',
'result': [
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 22.17210568, 'T': '2017-08-30T10:40:00', 'BV': 0.01448082},
{'O': 0.00066194, 'H': 0.00066195, 'L': 0.00066194, 'C': 0.00066195, 'V': 33.4727437, 'T': '2017-08-30T10:34:00', 'BV': 0.02215696},
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 53.85127609, 'T': '2017-08-30T10:37:00', 'BV': 0.0351708},
{'O': 0.00066194, 'H': 0.00066194, 'L': 0.00065311, 'C': 0.00065311, 'V': 46.29210665, 'T': '2017-08-30T10:42:00', 'BV': 0.03063118},
]
}
class TestAnalyze(unittest.TestCase):
def setUp(self):
self.result = parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00'))
def test_1_dataframe_has_correct_columns(self):
self.assertEqual(self.result.columns.tolist(),
['close', 'high', 'low', 'open', 'date', 'volume'])
def test_2_orders_by_date(self):
self.assertEqual(self.result['date'].tolist(),
['2017-08-30T10:34:00',
'2017-08-30T10:37:00',
'2017-08-30T10:40:00',
'2017-08-30T10:42:00'])
def test_3_populates_buy_trend(self):
dataframe = populate_buy_trend(populate_indicators(self.result))
self.assertTrue('buy' in dataframe.columns)
self.assertTrue('buy_price' in dataframe.columns)
def test_4_returns_latest_buy_signal(self):
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
with patch('analyze.analyze_ticker', return_value=buydf):
self.assertEqual(get_buy_signal('BTC-ETH'), True)
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
with patch('analyze.analyze_ticker', return_value=buydf):
self.assertEqual(get_buy_signal('BTC-ETH'), False)
if __name__ == '__main__':
unittest.main()

View File

@@ -1,105 +0,0 @@
import unittest
from unittest.mock import patch, MagicMock
from jsonschema import validate
import exchange
from main import create_trade, handle_trade, close_trade_if_fulfilled, init
from misc import CONF_SCHEMA
from persistence import Trade
class TestMain(unittest.TestCase):
conf = {
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"dry_run": True,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"poloniex": {
"enabled": False,
"key": "key",
"secret": "secret",
"pair_whitelist": []
},
"bittrex": {
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH"
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "chat_id"
}
}
def test_1_create_trade(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True) as buy_signal:
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
trade = create_trade(15.0, exchange.Exchange.BITTREX)
Trade.session.add(trade)
Trade.session.flush()
self.assertIsNotNone(trade)
self.assertEqual(trade.open_rate, 0.072661)
self.assertEqual(trade.pair, 'BTC_ETH')
self.assertEqual(trade.exchange, exchange.Exchange.BITTREX)
self.assertEqual(trade.amount, 206.43811673387373)
self.assertEqual(trade.btc_amount, 15.0)
self.assertEqual(trade.is_open, True)
self.assertIsNotNone(trade.open_date)
buy_signal.assert_called_once_with('BTC_ETH')
def test_2_handle_trade(self):
with patch.dict('main._CONF', self.conf):
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.17256061,
'ask': 0.172661,
'last': 0.17256061
}),
buy=MagicMock(return_value='mocked_order_id')):
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
self.assertTrue(trade)
handle_trade(trade)
self.assertEqual(trade.close_rate, 0.17256061)
self.assertEqual(trade.close_profit, 137.4872490056564)
self.assertIsNotNone(trade.close_date)
self.assertEqual(trade.open_order_id, 'dry_run')
def test_3_close_trade(self):
with patch.dict('main._CONF', self.conf):
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
self.assertTrue(trade)
# Simulate that there is no open order
trade.open_order_id = None
closed = close_trade_if_fulfilled(trade)
self.assertTrue(closed)
self.assertEqual(trade.is_open, False)
@classmethod
def setUpClass(cls):
validate(cls.conf, CONF_SCHEMA)
if __name__ == '__main__':
unittest.main()

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