Compare commits
648 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
20ebd744c3 | ||
|
70f2aed0a7 | ||
|
3087ca0823 | ||
|
372dc5b49a | ||
|
030aedc7d4 | ||
|
a26cb4bc6b | ||
|
1fe86656e1 | ||
|
39c6e5263a | ||
|
46a1a2de10 | ||
|
3a902289f1 | ||
|
3ac3ead2cf | ||
|
0d0737d1f6 | ||
|
27fcf62011 | ||
|
bb91fdbaf9 | ||
|
11cbb9188b | ||
|
c11102cf4a | ||
|
02fcbbb6d2 | ||
|
0d6051e6f9 | ||
|
6a433282dc | ||
|
8fb404b0f8 | ||
|
64530c6196 | ||
|
86db6c9084 | ||
|
0abc30401c | ||
|
1b6b0ad9d2 | ||
|
7cdbd550c8 | ||
|
69f68c428e | ||
|
abcdbcfd39 | ||
|
e67c652988 | ||
|
ddc711ec93 | ||
|
b9bf5c1118 | ||
|
9840e0b5b8 | ||
|
ffae0b2cd5 | ||
|
fe2b0c2862 | ||
|
1647e7a0c1 | ||
|
b35fa4c9f6 | ||
|
a48840509b | ||
|
ca8cab0ce9 | ||
|
bbcf6943ce | ||
|
fbf9bfe897 | ||
|
e46fcf0e02 | ||
|
f7dd5e6396 | ||
|
dd2ccea6e5 | ||
|
3d13eb2dc2 | ||
|
26b8661325 | ||
|
fa97a82568 | ||
|
1ae73d7da2 | ||
|
d8e692c9a3 | ||
|
ca05d1f79e | ||
|
9dd38aebe0 | ||
|
ceded8a20a | ||
|
9c21077dc1 | ||
|
fca6a09a41 | ||
|
bba711c89a | ||
|
5fbaa6d4cf | ||
|
5b1f84f816 | ||
|
65127533ef | ||
|
05ca00b623 | ||
|
4b6d855e63 | ||
|
7d7752efbf | ||
|
ce6f6ab9fe | ||
|
3a0569cfd3 | ||
|
7d21015b52 | ||
|
a57707071c | ||
|
2a347e4027 | ||
|
4c8ae3a7af | ||
|
2773ce7ebf | ||
|
f4e4104d14 | ||
|
b722a89276 | ||
|
4bf6711dbb | ||
|
5be733a174 | ||
|
c3cae5dfc4 | ||
|
0c9d862a49 | ||
|
975a785e68 | ||
|
6be607e528 | ||
|
80c4dea875 | ||
|
9e7a4c3717 | ||
|
c72e9c3cef | ||
|
8175eaa48a | ||
|
890083ce7f | ||
|
454cd16df4 | ||
|
7e233b536c | ||
|
ae19ab3dd3 | ||
|
bf4b2dc05e | ||
|
571ea6a2bc | ||
|
b3ea0f4ec5 | ||
|
d4c8ad5ba7 | ||
|
2432c9f290 | ||
|
7f7d53adb7 | ||
|
60ed4b9d1e | ||
|
83a999d16e | ||
|
a29f3de025 | ||
|
6ab0ec6aac | ||
|
984204e380 | ||
|
297166fcb9 | ||
|
e6e57e47cf | ||
|
bcde377019 | ||
|
2d39759d34 | ||
|
e4500af736 | ||
|
41933c31ca | ||
|
47675943ee | ||
|
74a708b794 | ||
|
833c7f21af | ||
|
f8eedc69dd | ||
|
797324c35e | ||
|
ae967a4f40 | ||
|
188fc69e56 | ||
|
be8506b45e | ||
|
79fcd0b06c | ||
|
421ccb23d3 | ||
|
f1969175cd | ||
|
7fd6d089c0 | ||
|
552fba773d | ||
|
8e272cfd53 | ||
|
36fbe54634 | ||
|
90017998fc | ||
|
ebe95ba1e1 | ||
|
c803762704 | ||
|
f8d8f3347a | ||
|
d4fcc38a57 | ||
|
c60ef181dc | ||
|
db4ad2f6f9 | ||
|
b5d2cfecc7 | ||
|
75955fcc04 | ||
|
050e73d960 | ||
|
0f2d3adbbc | ||
|
ea6a1c629d | ||
|
eb53a796e2 | ||
|
2d273a8509 | ||
|
7169ad557f | ||
|
b4d6250d55 | ||
|
45f2d01895 | ||
|
c176ace889 | ||
|
1ce4613aad | ||
|
eb473842b8 | ||
|
407eaa0870 | ||
|
9b09b5aa29 | ||
|
70d1511f73 | ||
|
4a717f3df8 | ||
|
cb7c36a512 | ||
|
f37c495b90 | ||
|
284c6c4223 | ||
|
fd5497cfc7 | ||
|
208d3770da | ||
|
01b49dc502 | ||
|
fbb19e451d | ||
|
a1ffa4497d | ||
|
e69f9dd029 | ||
|
fed3024302 | ||
|
dc2f048c98 | ||
|
f4ccd4609b | ||
|
1e3a29c049 | ||
|
82e9ed2ac2 | ||
|
ae52880f81 | ||
|
90236fb537 | ||
|
55d0d27756 | ||
|
d849694a70 | ||
|
29987c3ff6 | ||
|
5f696a0cce | ||
|
90d3c09536 | ||
|
3f65fc014e | ||
|
5344b711ea | ||
|
a3e827c144 | ||
|
52e267e864 | ||
|
165781a545 | ||
|
e10a3d1f9d | ||
|
0c11d4443f | ||
|
50be2fabbf | ||
|
7a2e9ef535 | ||
|
079f2e3609 | ||
|
0e0d613191 | ||
|
de68209f3b | ||
|
59546b623e | ||
|
0a5463fee8 | ||
|
cdfb18e9b4 | ||
|
1f635d3793 | ||
|
714d77dbd8 | ||
|
9803130848 | ||
|
ad44d8d42a | ||
|
68f81b2abb | ||
|
4945331093 | ||
|
8411844d7e | ||
|
00415d66a2 | ||
|
f7398e615a | ||
|
e81a9cbb17 | ||
|
c8c8c626b0 | ||
|
9f5f0ddaaa | ||
|
80e1e64eae | ||
|
37613fc056 | ||
|
57c6aefe38 | ||
|
133c467cf4 | ||
|
900cab4b42 | ||
|
f9cc556971 | ||
|
f2ce367cec | ||
|
fba9cbcff6 | ||
|
3e0458da7d | ||
|
0d605d2396 | ||
|
145583f0b7 | ||
|
847dde0d65 | ||
|
ab112581a7 | ||
|
f48f5d0f31 | ||
|
0abf0b0e39 | ||
|
965616b214 | ||
|
a36fd00f6a | ||
|
7f44ba6df4 | ||
|
7b0beb0afa | ||
|
ae0a1436e2 | ||
|
8537e9f40f | ||
|
d61d88559c | ||
|
9b4c0f01f2 | ||
|
6c8253a4f5 | ||
|
6464373636 | ||
|
dcd0a0ec61 | ||
|
ff6b0fc1c9 | ||
|
a514b92dcf | ||
|
de33d69eed | ||
|
9959d53f5e | ||
|
6768658300 | ||
|
433bf409f4 | ||
|
353b0d2d34 | ||
|
e644d57dbe | ||
|
50e7cef5f3 | ||
|
1058820e1b | ||
|
24bc3a8390 | ||
|
5309ea3820 | ||
|
a063680d32 | ||
|
10cf2ce853 | ||
|
871357a2e3 | ||
|
efe0d77dbb | ||
|
8d93363655 | ||
|
b6dd9dd227 | ||
|
95c6ada2ad | ||
|
11585f9581 | ||
|
8085a7b237 | ||
|
c99e2c12ba | ||
|
44a4ff0cb2 | ||
|
f300af0fe2 | ||
|
ff186c7f65 | ||
|
41e22657e4 | ||
|
974815cb14 | ||
|
33beab9c47 | ||
|
d258118b0a | ||
|
4dab39ed9e | ||
|
33293d5cdd | ||
|
285308dcbe | ||
|
c8fb6c4661 | ||
|
1a556198b2 | ||
|
98650acca0 | ||
|
123f2781a1 | ||
|
92f6db5bd7 | ||
|
e5f8c1e75d | ||
|
4c0a316e3e | ||
|
d613d63fdc | ||
|
e3941cde7e | ||
|
642422d5c4 | ||
|
117ec4e64d | ||
|
0219584bfe | ||
|
d3947fc893 | ||
|
fe0c26f536 | ||
|
e83e4909a0 | ||
|
ed05a1db9d | ||
|
21a11f5589 | ||
|
6288adfefd | ||
|
6a1caafb7a | ||
|
ce51749177 | ||
|
a68ca31684 | ||
|
5f1b9943d1 | ||
|
155ed4e501 | ||
|
80ef2cfed4 | ||
|
5efc417690 | ||
|
14868615d5 | ||
|
512fcdbcb1 | ||
|
6f2caf9698 | ||
|
a395a14eeb | ||
|
95fe0f4dec | ||
|
f6d85e021f | ||
|
597f08e2a2 | ||
|
df4784e7b9 | ||
|
ddd3d2d0a9 | ||
|
cb4ecfd3a3 | ||
|
f4b59492ab | ||
|
ae37f49b51 | ||
|
6e68315d2c | ||
|
c1c9dd03ce | ||
|
e00f02b603 | ||
|
9f907d5b5e | ||
|
6729574201 | ||
|
2a2af4878e | ||
|
bfb3e09d1d | ||
|
89ee0654f4 | ||
|
2ac8b685d6 | ||
|
4b38100ae2 | ||
|
d6c14d5258 | ||
|
cb09cabbdd | ||
|
77023c0ecf | ||
|
0b18c93d19 | ||
|
0617753a7f | ||
|
b77fad6e5f | ||
|
90bf6f2d4a | ||
|
ef7646417b | ||
|
01874e379f | ||
|
7afd8da28f | ||
|
3d532c6015 | ||
|
a692ef6715 | ||
|
ccb8c3c352 | ||
|
18f01113c2 | ||
|
f7def09dec | ||
|
82bf0be3e2 | ||
|
212f4fdd95 | ||
|
a5058ff999 | ||
|
ea1c16f2ac | ||
|
67337fadaa | ||
|
94c1d66e59 | ||
|
510e6edfbf | ||
|
e8c31142ae | ||
|
71c780a530 | ||
|
7e579de163 | ||
|
dd1a52c534 | ||
|
e815a43164 | ||
|
2f17706e76 | ||
|
86a94798dd | ||
|
a7cca4985e | ||
|
965c075362 | ||
|
05d7746f62 | ||
|
688326b58c | ||
|
0c9993cc89 | ||
|
0c35e6ad19 | ||
|
68521ea46c | ||
|
2fe11cd77a | ||
|
e27a6a7a91 | ||
|
5bf583cba4 | ||
|
a23fce519d | ||
|
7f3f127165 | ||
|
9ff1f05e66 | ||
|
b9c4eafd96 | ||
|
7fa5846c6b | ||
|
3b37f77a4d | ||
|
858d2329e5 | ||
|
371ee1e457 | ||
|
cfbfe90aa0 | ||
|
fd30f5dc59 | ||
|
0502bd3c2d | ||
|
3ce7ef5e8b | ||
|
2324aa0782 | ||
|
6a57a8da12 | ||
|
9276f3202c | ||
|
a6598997e2 | ||
|
82913cd3f4 | ||
|
be6939ee8a | ||
|
7ba4a5d24b | ||
|
371e6d99c9 | ||
|
84b105c82b | ||
|
c6def418cf | ||
|
5fce2c5712 | ||
|
aacd7d8987 | ||
|
4a707d7452 | ||
|
21551b3c40 | ||
|
7727f2cc8f | ||
|
9a87dcf0a1 | ||
|
9136e64d89 | ||
|
765a762ccf | ||
|
02ca2ed585 | ||
|
f3ba3ddd54 | ||
|
65ce948b0b | ||
|
383a9f6eeb | ||
|
43dda9c9cf | ||
|
7a44a1d1c1 | ||
|
5d934cd5b6 | ||
|
788cda4925 | ||
|
55a69e4a45 | ||
|
1931d31147 | ||
|
e9dbdc9247 | ||
|
86b6c6f334 | ||
|
cd5afd6ff4 | ||
|
d88cc084e6 | ||
|
5deaebf0c2 | ||
|
19734ad863 | ||
|
b16ccb9919 | ||
|
d41837817c | ||
|
3ab14dfe39 | ||
|
4a91ecd91a | ||
|
a3da2911e8 | ||
|
6f5b418f0b | ||
|
57691c82b1 | ||
|
37a74b38ba | ||
|
9ab81a987d | ||
|
4b08e3d571 | ||
|
187fea0c28 | ||
|
4e54b27398 | ||
|
aced5cc3ba | ||
|
669ec30413 | ||
|
0082b7abdd | ||
|
7903f3a546 | ||
|
ec75586bdd | ||
|
df9902d6a4 | ||
|
315919cdd6 | ||
|
63c95a3546 | ||
|
59d04d1d0c | ||
|
d1cc9e868b | ||
|
14de46576b | ||
|
bdff29a472 | ||
|
8655c6c264 | ||
|
3f4e4a23a0 | ||
|
b682262486 | ||
|
5be7be6189 | ||
|
3475a07522 | ||
|
fb7ea169d4 | ||
|
5469293e5f | ||
|
9b644b0305 | ||
|
0df1404d6a | ||
|
bb4a9ed20f | ||
|
77887d6fbc | ||
|
d89db50465 | ||
|
632d00e01d | ||
|
2a56031cdc | ||
|
16d412323c | ||
|
27a6b29c80 | ||
|
5d1f874041 | ||
|
174122a09b | ||
|
1b6a60ecb2 | ||
|
1ccb266032 | ||
|
a963f1820c | ||
|
b9983149ef | ||
|
c1ef3f526c | ||
|
6b7afb80b2 | ||
|
0b8afa12e9 | ||
|
1db0a7d4ce | ||
|
c12a9ebd92 | ||
|
d86dcc4752 | ||
|
0bc96241d5 | ||
|
a0bb7a61e6 | ||
|
b115963a70 | ||
|
2e953a937d | ||
|
4e05691cab | ||
|
b5f58724a0 | ||
|
b83309b55d | ||
|
e8101a6da5 | ||
|
dd9cb008fb | ||
|
81f7172c4a | ||
|
bab59fbacd | ||
|
0f0b10b6cc | ||
|
8e68c5358e | ||
|
660f01b514 | ||
|
13537e3ce4 | ||
|
2963a90008 | ||
|
15b20b83fa | ||
|
1c3c316e45 | ||
|
517879382b | ||
|
bcd3340a80 | ||
|
12ae1e111e | ||
|
d3b3370f23 | ||
|
8f817a3634 | ||
|
cf79b15651 | ||
|
a4284351e3 | ||
|
906caf329b | ||
|
3db13fae13 | ||
|
274972f7af | ||
|
83fd27e031 | ||
|
3126dcfcea | ||
|
72aec6c320 | ||
|
b709ccbf53 | ||
|
7e99b13742 | ||
|
8b464033ff | ||
|
93c525a8fa | ||
|
54b15c1556 | ||
|
029f32af63 | ||
|
de13df6ede | ||
|
0de211674d | ||
|
f7a27c156c | ||
|
98f11fc7bb | ||
|
013e13e546 | ||
|
6ff26c561a | ||
|
c81358c291 | ||
|
ed34d9f22f | ||
|
ee05561ef3 | ||
|
69ae99406a | ||
|
0cfbb56b6c | ||
|
8960373f1c | ||
|
349a91bd92 | ||
|
991b43b7e5 | ||
|
a0fa6abcdc | ||
|
86501b43c0 | ||
|
80592970e9 | ||
|
567ed4ecda | ||
|
fafbb0abfe | ||
|
0f1a36b8e9 | ||
|
31c03cdce1 | ||
|
e01c85bb3a | ||
|
a1b91ad1ea | ||
|
6ce6018bb7 | ||
|
18eec0f4d4 | ||
|
32327c45c2 | ||
|
ba485fe2b2 | ||
|
f8084b117e | ||
|
abdddd5193 | ||
|
8eeb02e592 | ||
|
8555271102 | ||
|
d921bae75e | ||
|
a1388ef296 | ||
|
ddc7c94a1d | ||
|
e36444df27 | ||
|
0395c92260 | ||
|
f03395b90d | ||
|
20d5628786 | ||
|
57e089efd3 | ||
|
fbbde9de25 | ||
|
3d42b9fd75 | ||
|
adfae9e75c | ||
|
117dfbb563 | ||
|
e66dc8b027 | ||
|
ae0b49f532 | ||
|
a37ea13fd1 | ||
|
cc29126d61 | ||
|
810f2f9243 | ||
|
60e651cb4c | ||
|
472ce8566d | ||
|
27ac15f298 | ||
|
d12dba16db | ||
|
0f1d114c03 | ||
|
3e7700e9ac | ||
|
60615c232c | ||
|
3884cfb809 | ||
|
caa6e22e53 | ||
|
19f6ff330c | ||
|
8fdd127f72 | ||
|
0a5eba64e2 | ||
|
b82c4444b2 | ||
|
95a17b8f98 | ||
|
325f72fd91 | ||
|
a237225683 | ||
|
29b173f4e7 | ||
|
50a979161c | ||
|
264d71e29e | ||
|
a873688a44 | ||
|
7cc8533b8e | ||
|
04342acff1 | ||
|
c37df0e70d | ||
|
460dfa1031 | ||
|
08a1d3ca1d | ||
|
1daeed4a52 | ||
|
99724e2458 | ||
|
cd18629433 | ||
|
41510fdb32 | ||
|
9cb249610a | ||
|
543857ddb2 | ||
|
1e5b0e8726 | ||
|
0d0d822904 | ||
|
9ff4a7b205 | ||
|
0e96197a94 | ||
|
9b9d0250f7 | ||
|
4a35676794 | ||
|
465c91b9a9 | ||
|
60249af04c | ||
|
c3653dc417 | ||
|
3d61095ba4 | ||
|
7a0be94cde | ||
|
fad6427078 | ||
|
4dfde7f9a2 | ||
|
e2eceaa904 | ||
|
f34af0ad67 | ||
|
e07904d436 | ||
|
26468bef83 | ||
|
ea1b1e11ea | ||
|
e68e6c0a1a | ||
|
7190226c84 | ||
|
6f2915e25e | ||
|
6f7ac0720b | ||
|
b76554a487 | ||
|
8da55c3742 | ||
|
05111edd04 | ||
|
361bdd20d3 | ||
|
8bdace68f6 | ||
|
0e1eb20781 | ||
|
4c2dea83c5 | ||
|
d066817d0b | ||
|
a632121368 | ||
|
473d09b5cd | ||
|
893738d6f0 | ||
|
22cfef7d36 | ||
|
e1bbe1d9a9 | ||
|
ec981b415a | ||
|
57a17697a0 | ||
|
f4fe09ffbf | ||
|
871b5e17ee | ||
|
9b00fc3474 | ||
|
3b1dc36d8a | ||
|
4edf8f2079 | ||
|
54987fd9ca | ||
|
da9c3e7d7d | ||
|
a948142ef5 | ||
|
4f6c3f94e0 | ||
|
25d6d6bbe5 | ||
|
649781d823 | ||
|
08ca7a8166 | ||
|
dd78c62c3d | ||
|
29de1645fe | ||
|
4139b0b0c7 | ||
|
0c33e917d5 | ||
|
e401a016f5 | ||
|
e0fde8665c | ||
|
752520c823 | ||
|
6ba2492360 | ||
|
d5d798f6fa | ||
|
9c9cf76a0d | ||
|
041e201713 | ||
|
e09505b22d | ||
|
6b15cb9b10 | ||
|
ff4fcdc760 | ||
|
f43ba44b15 | ||
|
79c3e0583d | ||
|
f6ef8383bb | ||
|
6f5307fda7 | ||
|
37004e331a | ||
|
57acf85b42 | ||
|
96790d50e5 | ||
|
d32ff3410c | ||
|
35838f5e64 | ||
|
913488910c | ||
|
17b984a7cd | ||
|
f79b44eefe | ||
|
146c254c0f | ||
|
ce2966dd7f | ||
|
0fbca8b8ef | ||
|
3f7a583de6 | ||
|
1196983d5f | ||
|
bbb2c7cf62 | ||
|
ff100bdac4 | ||
|
4feb038d0a | ||
|
1792e0fb9b | ||
|
d4f8b3ebbc | ||
|
aeef9bac33 | ||
|
eff361a104 | ||
|
389f9b45bb | ||
|
c9741cb291 | ||
|
bf6f563df2 | ||
|
58f34d4f4b | ||
|
2c4d0144ba | ||
|
afd1a0bf9b | ||
|
37f6c213f6 | ||
|
76736902c6 | ||
|
d266171ed8 | ||
|
e7df373544 | ||
|
aa4b64d0bb | ||
|
4559ddd74f | ||
|
eecc45f8ba | ||
|
d76476040a | ||
|
0c8c149b86 | ||
|
60a7f8614c | ||
|
c31b67bf7a |
@@ -1,2 +1,5 @@
|
||||
[run]
|
||||
omit = freqtrade/tests/*
|
||||
omit =
|
||||
scripts/*
|
||||
freqtrade/tests/*
|
||||
freqtrade/vendor/*
|
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
config.json*
|
||||
*.sqlite
|
30
.github/ISSUE_TEMPLATE.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
14
.gitignore
vendored
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
25
.travis.yml
25
.travis.yml
@@ -1,4 +1,4 @@
|
||||
sudo: false
|
||||
sudo: true
|
||||
os:
|
||||
- linux
|
||||
language: python
|
||||
@@ -11,16 +11,27 @@ addons:
|
||||
- libdw-dev
|
||||
- binutils-dev
|
||||
install:
|
||||
- wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||
- tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||
- cd ta-lib && ./configure && sudo make && sudo make install && cd ..
|
||||
- ./install_ta-lib.sh
|
||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||
- pip install coveralls
|
||||
- pip install --upgrade flake8 coveralls
|
||||
- pip install -r requirements.txt
|
||||
script:
|
||||
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||
- pip install -e .
|
||||
jobs:
|
||||
include:
|
||||
- script: pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||
- script:
|
||||
- cp config.json.example config.json
|
||||
- python freqtrade/main.py backtesting
|
||||
- script:
|
||||
- cp config.json.example config.json
|
||||
- python freqtrade/main.py hyperopt -e 5
|
||||
- script: flake8 freqtrade
|
||||
after_success:
|
||||
- coveralls
|
||||
notifications:
|
||||
slack:
|
||||
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/pip
|
||||
- ta-lib
|
45
CONTRIBUTING.md
Normal file
45
CONTRIBUTING.md
Normal 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
|
||||
```
|
||||
|
||||
|
21
Dockerfile
21
Dockerfile
@@ -1,20 +1,23 @@
|
||||
FROM python:3.6.2
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install build-essential
|
||||
|
||||
# Install TA-lib
|
||||
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
|
||||
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
|
||||
|
||||
# Prepare environment
|
||||
RUN mkdir /freqtrade
|
||||
COPY . /freqtrade/
|
||||
WORKDIR /freqtrade
|
||||
|
||||
# Install dependencies and execute
|
||||
# Install dependencies
|
||||
COPY requirements.txt /freqtrade/
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# Install and execute
|
||||
COPY . /freqtrade/
|
||||
RUN pip install -e .
|
||||
CMD ["freqtrade"]
|
||||
ENTRYPOINT ["freqtrade"]
|
||||
|
@@ -1,7 +1,5 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include config.json.example
|
||||
include freqtrade/exchange/*.py
|
||||
include freqtrade/rpc/*.py
|
||||
include freqtrade/tests/*.py
|
||||
recursive-include freqtrade *.py
|
||||
include freqtrade/tests/testdata/*.json
|
||||
|
247
README.md
247
README.md
@@ -1,103 +1,198 @@
|
||||
# freqtrade
|
||||
|
||||
[](https://travis-ci.org/gcarq/freqtrade)
|
||||
[](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
|
||||
[](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
|
||||
|
||||
|
||||
Simple High frequency trading bot for crypto currencies.
|
||||
Currently supports trading on Bittrex exchange.
|
||||
Simple High frequency trading bot for crypto currencies designed to
|
||||
support multi exchanges and be controlled via Telegram.
|
||||
|
||||
This software is for educational purposes only.
|
||||
Don't risk money which you are afraid to lose.
|
||||

|
||||
|
||||
The command interface is accessible via Telegram (not required).
|
||||
Just register a new bot on https://telegram.me/BotFather
|
||||
and enter the telegram `token` and your `chat_id` in `config.json`
|
||||
## Disclaimer
|
||||
This software is for educational purposes only. Do not risk money which
|
||||
you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS
|
||||
AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS.
|
||||
|
||||
Persistence is achieved through sqlite.
|
||||
Always start by running a trading bot in Dry-run and do not engage money
|
||||
before you understand how it works and what profit/loss you should
|
||||
expect.
|
||||
|
||||
#### Telegram RPC commands:
|
||||
* /start: Starts the trader
|
||||
* /stop: Stops the trader
|
||||
* /status: 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
|
||||
We strongly recommend you to have coding and Python knowledge. Do not
|
||||
hesitate to read the source code and understand the mechanism of this bot.
|
||||
|
||||
#### Config
|
||||
`minimal_roi` is a JSON object where the key is a duration
|
||||
in minutes and the value is the minimum ROI in percent.
|
||||
See the example below:
|
||||
## Table of Contents
|
||||
- [Features](#features)
|
||||
- [Quick start](#quick-start)
|
||||
- [Documentations](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
||||
- [Installation](https://github.com/gcarq/freqtrade/blob/develop/docs/installation.md)
|
||||
- [Configuration](https://github.com/gcarq/freqtrade/blob/develop/docs/configuration.md)
|
||||
- [Strategy Optimization](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-optimization.md)
|
||||
- [Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/backtesting.md)
|
||||
- [Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/hyperopt.md)
|
||||
- [Support](#support)
|
||||
- [Help](#help--slack)
|
||||
- [Bugs](#bugs--issues)
|
||||
- [Feature Requests](#feature-requests)
|
||||
- [Pull Requests](#pull-requests)
|
||||
- [Basic Usage](#basic-usage)
|
||||
- [Bot commands](#bot-commands)
|
||||
- [Telegram RPC commands](#telegram-rpc-commands)
|
||||
- [Requirements](#requirements)
|
||||
- [Min hardware required](#min-hardware-required)
|
||||
- [Software requirements](#software-requirements)
|
||||
|
||||
## Branches
|
||||
The project is currently setup in two main branches:
|
||||
- `develop` - This branch has often new features, but might also cause
|
||||
breaking changes.
|
||||
- `master` - This branch contains the latest stable release. The bot
|
||||
'should' be stable on this branch, and is generally well tested.
|
||||
|
||||
## Features
|
||||
- [x] **Based on Python 3.6+**: For botting on any operating system -
|
||||
Windows, macOS and Linux
|
||||
- [x] **Persistence**: Persistence is achieved through sqlite
|
||||
- [x] **Dry-run**: Run the bot without playing money.
|
||||
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
|
||||
- [x] **Strategy Optimization**: Optimize your buy/sell strategy
|
||||
parameters with Hyperopts.
|
||||
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you
|
||||
want to trade.
|
||||
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you
|
||||
want to avoid.
|
||||
- [x] **Manageable via Telegram**: Manage the bot with Telegram
|
||||
- [x] **Display profit/loss in fiat**: Display your profit/loss in
|
||||
33 fiat.
|
||||
- [x] **Daily summary of profit/loss**: Provide a daily summary
|
||||
of your profit/loss.
|
||||
- [x] **Performance status report**: Provide a performance status of
|
||||
your current trades.
|
||||
|
||||
### Exchange supported
|
||||
- [x] Bittrex
|
||||
- [ ] Binance
|
||||
- [ ] Others
|
||||
|
||||
## Quick start
|
||||
This quick start section is a very short explanation on how to test the
|
||||
bot in dry-run. We invite you to read the
|
||||
[bot documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
||||
to ensure you understand how the bot is working.
|
||||
|
||||
The following steps are made for Linux/MacOS environment
|
||||
|
||||
**1. Clone the repo**
|
||||
```bash
|
||||
git clone git@github.com:gcarq/freqtrade.git
|
||||
git checkout develop
|
||||
cd freqtrade
|
||||
```
|
||||
"minimal_roi": {
|
||||
"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).
|
||||
|
||||
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
||||
use the `last` price and values between those interpolate between ask and last price. Using `ask` price will guarantee quick success in bid, but bot will also end up paying more then would probably have been necessary.
|
||||
### [Bugs / Issues](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
|
||||
If you discover a bug in the bot, please
|
||||
[search our issue tracker](https://github.com/gcarq/freqtrade/issues?q=is%3Aissue)
|
||||
first. If it hasn't been reported, please
|
||||
[create a new issue](https://github.com/gcarq/freqtrade/issues/new) and
|
||||
ensure you follow the template guide so that our team can assist you as
|
||||
quickly as possible.
|
||||
|
||||
The other values should be self-explanatory,
|
||||
if not feel free to raise a github issue.
|
||||
### [Feature Requests](https://github.com/gcarq/freqtrade/labels/enhancement)
|
||||
Have you a great idea to improve the bot you want to share? Please,
|
||||
first search if this feature was not [already discussed](https://github.com/gcarq/freqtrade/labels/enhancement).
|
||||
If it hasn't been requested, please
|
||||
[create a new request](https://github.com/gcarq/freqtrade/issues/new)
|
||||
and ensure you follow the template guide so that it does not get lost
|
||||
in the bug reports.
|
||||
|
||||
#### Prerequisites
|
||||
* python3.6
|
||||
* sqlite
|
||||
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
||||
### [Pull Requests](https://github.com/gcarq/freqtrade/pulls)
|
||||
Feel like our bot is missing a feature? We welcome your pull requests!
|
||||
Please read our
|
||||
[Contributing document](https://github.com/gcarq/freqtrade/blob/develop/CONTRIBUTING.md)
|
||||
to understand the requirements before sending your pull-requests.
|
||||
|
||||
#### Install
|
||||
**Important:** Always create your PR against the `develop` branch, not
|
||||
`master`.
|
||||
|
||||
`master` branch contains the latest stable release.
|
||||
## Basic Usage
|
||||
|
||||
`develop` branch has often new features, but might also cause breaking changes. To use it, you are encouraged to join our [slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
||||
### 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.
|
||||
```
|
||||
$ cd freqtrade/
|
||||
# copy example config. Dont forget to insert your api keys
|
||||
$ cp config.json.example config.json
|
||||
$ python -m venv .env
|
||||
$ source .env/bin/activate
|
||||
$ pip install -r requirements.txt
|
||||
$ pip install -e .
|
||||
$ ./freqtrade/main.py
|
||||
```
|
||||
More details on:
|
||||
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)
|
||||
- [How to use Backtesting](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#backtesting-commands)
|
||||
- [How to use Hyperopt](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#hyperopt-commands)
|
||||
|
||||
There is also an [article](https://www.sales4k.com/blockchain/high-frequency-trading-bot-tutorial/) about how to setup the bot (thanks [@gurghet](https://github.com/gurghet)).
|
||||
### Telegram RPC commands
|
||||
Telegram is not mandatory. However, this is a great way to control your
|
||||
bot. More details on our
|
||||
[documentation](https://github.com/gcarq/freqtrade/blob/develop/docs/index.md)
|
||||
|
||||
#### 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
|
||||
|
||||
```
|
||||
$ pytest
|
||||
```
|
||||
This will by default skip the slow running backtest set. To run backtest set:
|
||||
## Requirements
|
||||
|
||||
```
|
||||
$ BACKTEST=true pytest
|
||||
```
|
||||
### 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
|
||||
|
||||
#### Docker
|
||||
```
|
||||
$ cd freqtrade
|
||||
$ docker build -t freqtrade .
|
||||
$ docker run --rm -it freqtrade
|
||||
```
|
||||
|
||||
#### Contributing
|
||||
|
||||
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
|
||||
|
||||
- Create your PR against the `develop` branch, not `master`.
|
||||
- New features need to contain unit tests.
|
||||
- If you are unsure, discuss the feature on [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.
|
||||
### Software requirements
|
||||
- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||
- [pip](https://pip.pypa.io/en/stable/installing/)
|
||||
- [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
- [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html)
|
||||
- [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||
- [Docker](https://www.docker.com/products/docker) (Recommended)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from freqtrade.main import main
|
||||
main()
|
@@ -2,14 +2,16 @@
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.05,
|
||||
"fiat_display_currency": "USD",
|
||||
"dry_run": false,
|
||||
"minimal_roi": {
|
||||
"60": 0.0,
|
||||
"40": 0.01,
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.03
|
||||
"0": 0.04
|
||||
},
|
||||
"stoploss": -0.40,
|
||||
"stoploss": -0.10,
|
||||
"unfilledtimeout": 600,
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
@@ -18,21 +20,32 @@
|
||||
"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
|
||||
}
|
||||
}
|
0
docs/.gitkeep
Normal file
0
docs/.gitkeep
Normal file
BIN
docs/assets/freqtrade-screenshot.png
Normal file
BIN
docs/assets/freqtrade-screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 142 KiB |
109
docs/backtesting.md
Normal file
109
docs/backtesting.md
Normal 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
117
docs/bot-optimization.md
Normal 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
139
docs/bot-usage.md
Normal 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
128
docs/configuration.md
Normal 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
21
docs/faq.md
Normal 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.
|
||||
|
||||
#### I’d 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
294
docs/hyperopt.md
Normal 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
32
docs/index.md
Normal 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
246
docs/installation.md
Normal 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
46
docs/pre-requisite.md
Normal 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
78
docs/sql_cheatsheet.md
Normal 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
129
docs/telegram-usage.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Telegram usage
|
||||
|
||||
This page explains how to command your bot with Telegram.
|
||||
|
||||
## Pre-requisite
|
||||
To control your bot with Telegram, you need first to
|
||||
[set up a Telegram bot](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md)
|
||||
and add your Telegram API keys into your config file.
|
||||
|
||||
## Telegram commands
|
||||
Per default, the Telegram bot shows predefined commands. Some commands
|
||||
are only available by sending them to the bot. The table below list the
|
||||
official commands. You can ask at any moment for help with `/help`.
|
||||
|
||||
| Command | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `/start` | | Starts the trader
|
||||
| `/stop` | | Starts the trader
|
||||
| `/status` | | Lists all open trades
|
||||
| `/status table` | | List all open trades in a table format
|
||||
| `/count` | | Displays number of trades used and available
|
||||
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
| `/forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
|
||||
| `/performance` | | Show performance of each finished trade grouped by pair
|
||||
| `/balance` | | Show account balance per currency
|
||||
| `/daily <n>` | 7 | Shows profit or loss per day, over the last n days
|
||||
| `/help` | | Show help message
|
||||
| `/version` | | Show version
|
||||
|
||||
## Telegram commands in action
|
||||
Below, example of Telegram message you will receive for each command.
|
||||
|
||||
### /start
|
||||
> **Status:** `running`
|
||||
|
||||
### /stop
|
||||
> `Stopping trader ...`
|
||||
> **Status:** `stopped`
|
||||
|
||||
## /status
|
||||
For each open trade, the bot will send you the following message.
|
||||
|
||||
> **Trade ID:** `123`
|
||||
> **Current Pair:** BTC_CVC
|
||||
> **Open Since:** `1 days ago`
|
||||
> **Amount:** `26.64180098`
|
||||
> **Open Rate:** `0.00007489`
|
||||
> **Close Rate:** `None`
|
||||
> **Current Rate:** `0.00007489`
|
||||
> **Close Profit:** `None`
|
||||
> **Current Profit:** `12.95%`
|
||||
> **Open Order:** `None`
|
||||
|
||||
## /status table
|
||||
Return the status of all open trades in a table format.
|
||||
```
|
||||
ID Pair Since Profit
|
||||
---- -------- ------- --------
|
||||
67 BTC_SC 1 d 13.33%
|
||||
123 BTC_CVC 1 h 12.95%
|
||||
```
|
||||
|
||||
## /count
|
||||
Return the number of trades used and available.
|
||||
```
|
||||
current max
|
||||
--------- -----
|
||||
2 10
|
||||
```
|
||||
|
||||
## /profit
|
||||
Return a summary of your profit/loss and performance.
|
||||
|
||||
> **ROI:** Close trades
|
||||
> ∙ `0.00485701 BTC (258.45%)`
|
||||
> ∙ `62.968 USD`
|
||||
> **ROI:** All trades
|
||||
> ∙ `0.00255280 BTC (143.43%)`
|
||||
> ∙ `33.095 EUR`
|
||||
>
|
||||
> **Total Trade Count:** `138`
|
||||
> **First Trade opened:** `3 days ago`
|
||||
> **Latest Trade opened:** `2 minutes ago`
|
||||
> **Avg. Duration:** `2:33:45`
|
||||
> **Best Performing:** `BTC_PAY: 50.23%`
|
||||
|
||||
## /forcesell <trade_id>
|
||||
|
||||
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
|
||||
|
||||
## /performance
|
||||
Return the performance of each crypto-currency the bot has sold.
|
||||
> Performance:
|
||||
> 1. `BTC_RCN 57.77%`
|
||||
> 2. `BTC_PAY 56.91%`
|
||||
> 3. `BTC_VIB 47.07%`
|
||||
> 4. `BTC_SALT 30.24%`
|
||||
> 5. `BTC_STORJ 27.24%`
|
||||
> ...
|
||||
|
||||
## /balance
|
||||
Return the balance of all crypto-currency your have on the exchange.
|
||||
|
||||
> **Currency:** BTC
|
||||
> **Available:** 3.05890234
|
||||
> **Balance:** 3.05890234
|
||||
> **Pending:** 0.0
|
||||
|
||||
> **Currency:** CVC
|
||||
> **Available:** 86.64180098
|
||||
> **Balance:** 86.64180098
|
||||
> **Pending:** 0.0
|
||||
|
||||
## /daily <n>
|
||||
Per default `/daily` will return the 7 last days.
|
||||
The example below if for `/daily 3`:
|
||||
|
||||
> **Daily Profit over the last 3 days:**
|
||||
```
|
||||
Day Profit BTC Profit USD
|
||||
---------- -------------- ------------
|
||||
2018-01-03 0.00224175 BTC 29,142 USD
|
||||
2018-01-02 0.00033131 BTC 4,307 USD
|
||||
2018-01-01 0.00269130 BTC 34.986 USD
|
||||
```
|
||||
|
||||
## /version
|
||||
> **Version:** `0.14.3`
|
14
freqtrade.service
Normal file
14
freqtrade.service
Normal 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
|
||||
|
@@ -1,3 +1,16 @@
|
||||
__version__ = '0.11.0'
|
||||
""" FreqTrade bot """
|
||||
__version__ = '0.16.0'
|
||||
|
||||
from . import main
|
||||
|
||||
class DependencyException(BaseException):
|
||||
"""
|
||||
Indicates that a assumed dependency is not met.
|
||||
This could happen when there is currently not enough money on the account.
|
||||
"""
|
||||
|
||||
|
||||
class OperationalException(BaseException):
|
||||
"""
|
||||
Requires manual intervention.
|
||||
This happens when an exchange returns an unexpected error during runtime.
|
||||
"""
|
||||
|
@@ -1,94 +1,304 @@
|
||||
"""
|
||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Dict, List
|
||||
|
||||
import arrow
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade.exchange import get_ticker_history
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame:
|
||||
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 pair
|
||||
:param pair: pair as str in format BTC_ETH or BTC-ETH
|
||||
Analyses the trend for the given ticker history
|
||||
:param ticker: See exchange.get_ticker_history
|
||||
:return: DataFrame
|
||||
"""
|
||||
df = DataFrame(ticker) \
|
||||
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
|
||||
frame = 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]
|
||||
.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.
|
||||
"""
|
||||
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.22)
|
||||
|
||||
# Momentum Indicator
|
||||
# ------------------------------------
|
||||
|
||||
# ADX
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
stoch = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch['fastd']
|
||||
dataframe['fastk'] = stoch['fastk']
|
||||
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||
dataframe['cci'] = ta.CCI(dataframe, timeperiod=5)
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=100)
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=4)
|
||||
|
||||
# Awesome oscillator
|
||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||
"""
|
||||
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
||||
dataframe['cci'] = ta.CCI(dataframe)
|
||||
"""
|
||||
# MACD
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
|
||||
# MFI
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
|
||||
# Minus Directional Indicator / Movement
|
||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||
|
||||
# Plus Directional Indicator / Movement
|
||||
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
|
||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
||||
"""
|
||||
# ROC
|
||||
dataframe['roc'] = ta.ROC(dataframe)
|
||||
"""
|
||||
# RSI
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
"""
|
||||
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
|
||||
rsi = 0.1 * (dataframe['rsi'] - 50)
|
||||
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
|
||||
|
||||
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
|
||||
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
|
||||
|
||||
# Stoch
|
||||
stoch = ta.STOCH(dataframe)
|
||||
dataframe['slowd'] = stoch['slowd']
|
||||
dataframe['slowk'] = stoch['slowk']
|
||||
"""
|
||||
# Stoch fast
|
||||
stoch_fast = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch_fast['fastd']
|
||||
dataframe['fastk'] = stoch_fast['fastk']
|
||||
"""
|
||||
# Stoch RSI
|
||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||
dataframe['fastk_rsi'] = stoch_rsi['fastk']
|
||||
"""
|
||||
|
||||
# Overlap Studies
|
||||
# ------------------------------------
|
||||
|
||||
# Previous Bollinger bands
|
||||
# Because ta.BBANDS implementation is broken with small numbers, it actually
|
||||
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
|
||||
# and use middle band instead.
|
||||
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||
"""
|
||||
# Bollinger bands
|
||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||
dataframe['bb_lowerband'] = bollinger['lower']
|
||||
dataframe['bb_middleband'] = bollinger['mid']
|
||||
dataframe['bb_upperband'] = bollinger['upper']
|
||||
"""
|
||||
|
||||
# EMA - Exponential Moving Average
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
|
||||
# SAR Parabol
|
||||
dataframe['sar'] = ta.SAR(dataframe)
|
||||
|
||||
# SMA - Simple Moving Average
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||
|
||||
# TEMA - Triple Exponential Moving Average
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||
|
||||
# Cycle Indicator
|
||||
# ------------------------------------
|
||||
# Hilbert Transform Indicator - SineWave
|
||||
hilbert = ta.HT_SINE(dataframe)
|
||||
dataframe['htsine'] = hilbert['sine']
|
||||
dataframe['htleadsine'] = hilbert['leadsine']
|
||||
|
||||
# Pattern Recognition - Bullish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hammer: values [0, 100]
|
||||
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
|
||||
|
||||
# Inverted Hammer: values [0, 100]
|
||||
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
|
||||
|
||||
# Dragonfly Doji: values [0, 100]
|
||||
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
|
||||
|
||||
# Piercing Line: values [0, 100]
|
||||
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
|
||||
|
||||
# Morningstar: values [0, 100]
|
||||
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
|
||||
|
||||
# Three White Soldiers: values [0, 100]
|
||||
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Hanging Man: values [0, 100]
|
||||
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
|
||||
|
||||
# Shooting Star: values [0, 100]
|
||||
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
|
||||
|
||||
# Gravestone Doji: values [0, 100]
|
||||
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
|
||||
|
||||
# Dark Cloud Cover: values [0, 100]
|
||||
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
|
||||
|
||||
# Evening Doji Star: values [0, 100]
|
||||
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
|
||||
|
||||
# Evening Star: values [0, 100]
|
||||
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
|
||||
"""
|
||||
|
||||
# Pattern Recognition - Bullish/Bearish candlestick patterns
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Three Line Strike: values [0, -100, 100]
|
||||
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
|
||||
|
||||
# Spinning Top: values [0, -100, 100]
|
||||
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
|
||||
|
||||
# Engulfing: values [0, -100, 100]
|
||||
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
|
||||
|
||||
# Harami: values [0, -100, 100]
|
||||
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
|
||||
|
||||
# Three Outside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
|
||||
|
||||
# Three Inside Up/Down: values [0, -100, 100]
|
||||
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
|
||||
"""
|
||||
|
||||
# Chart type
|
||||
# ------------------------------------
|
||||
"""
|
||||
# Heikinashi stategy
|
||||
heikinashi = qtpylib.heikinashi(dataframe)
|
||||
dataframe['ha_open'] = heikinashi['open']
|
||||
dataframe['ha_close'] = heikinashi['close']
|
||||
dataframe['ha_high'] = heikinashi['high']
|
||||
dataframe['ha_low'] = heikinashi['low']
|
||||
"""
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy trend for the given dataframe
|
||||
Based on TA indicators, populates the buy signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
|
||||
dataframe.loc[
|
||||
(dataframe['close'] < dataframe['sma']) &
|
||||
(dataframe['cci'] < -100) &
|
||||
(dataframe['tema'] <= dataframe['blower']) &
|
||||
(dataframe['mfi'] < 30) &
|
||||
(dataframe['fastd'] < 20) &
|
||||
(dataframe['adx'] > 20),
|
||||
(
|
||||
(dataframe['rsi'] < 35) &
|
||||
(dataframe['fastd'] < 35) &
|
||||
(dataframe['adx'] > 30) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 65) &
|
||||
(dataframe['plus_di'] > 0.5)
|
||||
),
|
||||
'buy'] = 1
|
||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def analyze_ticker(pair: str) -> DataFrame:
|
||||
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Get ticker data for given currency pair, push it to a DataFrame and
|
||||
Based on TA indicators, populates the sell signal for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.loc[
|
||||
(
|
||||
(
|
||||
(qtpylib.crossed_above(dataframe['rsi'], 70)) |
|
||||
(qtpylib.crossed_above(dataframe['fastd'], 70))
|
||||
) &
|
||||
(dataframe['adx'] > 10) &
|
||||
(dataframe['minus_di'] > 0)
|
||||
) |
|
||||
(
|
||||
(dataframe['adx'] > 70) &
|
||||
(dataframe['minus_di'] > 0.5)
|
||||
),
|
||||
'sell'] = 1
|
||||
return dataframe
|
||||
|
||||
|
||||
def analyze_ticker(ticker_history: List[Dict]) -> DataFrame:
|
||||
"""
|
||||
Parses the given ticker history and returns a populated DataFrame
|
||||
add several TA indicators and buy signal to it
|
||||
:return DataFrame with ticker data and indicator data
|
||||
"""
|
||||
minimum_date = arrow.utcnow().shift(hours=-24)
|
||||
data = get_ticker_history(pair, minimum_date)
|
||||
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
|
||||
|
||||
if dataframe.empty:
|
||||
logger.warning('Empty dataframe for pair %s', pair)
|
||||
return dataframe
|
||||
|
||||
dataframe = parse_ticker_dataframe(ticker_history)
|
||||
dataframe = populate_indicators(dataframe)
|
||||
dataframe = populate_buy_trend(dataframe)
|
||||
dataframe = populate_sell_trend(dataframe)
|
||||
return dataframe
|
||||
|
||||
|
||||
def get_buy_signal(pair: str) -> bool:
|
||||
def get_signal(pair: str, signal: SignalType) -> bool:
|
||||
"""
|
||||
Calculates a buy signal based several technical analysis indicators
|
||||
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
|
||||
"""
|
||||
dataframe = analyze_ticker(pair)
|
||||
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
|
||||
@@ -100,51 +310,6 @@ def get_buy_signal(pair: str) -> bool:
|
||||
if signal_date < arrow.now() - timedelta(minutes=10):
|
||||
return False
|
||||
|
||||
signal = latest['buy'] == 1
|
||||
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
||||
return signal
|
||||
|
||||
|
||||
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
|
||||
|
||||
# Two subplots sharing x axis
|
||||
fig, (ax1, ax2) = plt.subplots(2, sharex=True)
|
||||
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
||||
ax1.plot(dataframe.index.values, dataframe['sar'], 'g_', label='pSAR')
|
||||
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['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()
|
||||
|
||||
# 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:
|
||||
test_pair = 'BTC_ETH'
|
||||
# for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
||||
# get_buy_signal(pair)
|
||||
plot_dataframe(analyze_ticker(test_pair), test_pair)
|
||||
time.sleep(60)
|
||||
result = latest[signal.value] == 1
|
||||
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
|
||||
return result
|
||||
|
@@ -1,18 +1,27 @@
|
||||
# pragma pylint: disable=W0603
|
||||
""" Cryptocurrency Exchanges support """
|
||||
import enum
|
||||
import logging
|
||||
from typing import List
|
||||
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
|
||||
EXCHANGE: Exchange = None
|
||||
_API: Exchange = None
|
||||
_CONF: dict = {}
|
||||
|
||||
# Holds all open sell orders for dry_run
|
||||
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
|
||||
|
||||
|
||||
class Exchanges(enum.Enum):
|
||||
"""
|
||||
@@ -29,7 +38,7 @@ def init(config: dict) -> None:
|
||||
:param config: config to use
|
||||
:return: None
|
||||
"""
|
||||
global _CONF, EXCHANGE
|
||||
global _CONF, _API
|
||||
|
||||
_CONF.update(config)
|
||||
|
||||
@@ -43,9 +52,9 @@ def init(config: dict) -> None:
|
||||
try:
|
||||
exchange_class = Exchanges[name.upper()].value
|
||||
except KeyError:
|
||||
raise RuntimeError('Exchange {} is not supported'.format(name))
|
||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
||||
|
||||
EXCHANGE = exchange_class(exchange_config)
|
||||
_API = exchange_class(exchange_config)
|
||||
|
||||
# Check if all pairs are available
|
||||
validate_pairs(config['exchange']['pair_whitelist'])
|
||||
@@ -54,62 +63,123 @@ def init(config: dict) -> None:
|
||||
def validate_pairs(pairs: List[str]) -> None:
|
||||
"""
|
||||
Checks if all given pairs are tradable on the current exchange.
|
||||
Raises RuntimeError if one pair is not available.
|
||||
Raises OperationalException if one pair is not available.
|
||||
:param pairs: list of pairs
|
||||
:return: None
|
||||
"""
|
||||
markets = EXCHANGE.get_markets()
|
||||
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 RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower()))
|
||||
raise OperationalException(
|
||||
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
|
||||
|
||||
|
||||
def buy(pair: str, rate: float, amount: float) -> str:
|
||||
if _CONF['dry_run']:
|
||||
return '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 EXCHANGE.buy(pair, rate, amount)
|
||||
return _API.buy(pair, rate, amount)
|
||||
|
||||
|
||||
def sell(pair: str, rate: float, amount: float) -> str:
|
||||
if _CONF['dry_run']:
|
||||
return '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 EXCHANGE.sell(pair, rate, amount)
|
||||
return _API.sell(pair, rate, amount)
|
||||
|
||||
|
||||
def get_balance(currency: str) -> float:
|
||||
if _CONF['dry_run']:
|
||||
return 999.9
|
||||
|
||||
return EXCHANGE.get_balance(currency)
|
||||
return _API.get_balance(currency)
|
||||
|
||||
|
||||
def get_ticker(pair: str) -> dict:
|
||||
return EXCHANGE.get_ticker(pair)
|
||||
def get_balances():
|
||||
if _CONF['dry_run']:
|
||||
return []
|
||||
|
||||
return _API.get_balances()
|
||||
|
||||
|
||||
def get_ticker_history(pair: str, minimum_date: arrow.Arrow):
|
||||
return EXCHANGE.get_ticker_history(pair, minimum_date)
|
||||
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 EXCHANGE.cancel_order(order_id)
|
||||
return _API.cancel_order(order_id)
|
||||
|
||||
|
||||
def get_open_orders(pair: str) -> List[dict]:
|
||||
def get_order(order_id: str) -> Dict:
|
||||
if _CONF['dry_run']:
|
||||
return []
|
||||
order = _DRY_RUN_OPEN_ORDERS[order_id]
|
||||
order.update({
|
||||
'id': order_id
|
||||
})
|
||||
return order
|
||||
|
||||
return EXCHANGE.get_open_orders(pair)
|
||||
return _API.get_order(order_id)
|
||||
|
||||
|
||||
def get_pair_detail_url(pair: str) -> str:
|
||||
return EXCHANGE.get_pair_detail_url(pair)
|
||||
return _API.get_pair_detail_url(pair)
|
||||
|
||||
|
||||
def get_markets() -> List[str]:
|
||||
return EXCHANGE.get_markets()
|
||||
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()
|
||||
|
@@ -1,17 +1,34 @@
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
import arrow
|
||||
import requests
|
||||
from bittrex.bittrex import Bittrex as _Bittrex
|
||||
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):
|
||||
"""
|
||||
@@ -19,96 +36,165 @@ class Bittrex(Exchange):
|
||||
"""
|
||||
# Base URL and API endpoints
|
||||
BASE_URL: str = 'https://www.bittrex.com'
|
||||
TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks'
|
||||
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
|
||||
# Ticker inveral
|
||||
TICKER_INTERVAL: str = 'fiveMin'
|
||||
# Sleep time to avoid rate limits, used in the main loop
|
||||
SLEEP_TIME: float = 25
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
def sleep_time(self) -> float:
|
||||
return self.SLEEP_TIME
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
global _API, _EXCHANGE_CONF
|
||||
global _API, _API_V2, _EXCHANGE_CONF
|
||||
|
||||
_EXCHANGE_CONF.update(config)
|
||||
_API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret'])
|
||||
_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 = {}
|
||||
|
||||
# Check if all pairs are available
|
||||
markets = self.get_markets()
|
||||
exchange_name = self.name
|
||||
for pair in _EXCHANGE_CONF['pair_whitelist']:
|
||||
if pair not in markets:
|
||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, exchange_name))
|
||||
@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']:
|
||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||
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']:
|
||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||
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']:
|
||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||
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_ticker(self, pair: str) -> dict:
|
||||
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']:
|
||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||
return {
|
||||
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))
|
||||
|
||||
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None):
|
||||
url = self.TICKER_METHOD
|
||||
headers = {
|
||||
# TODO: Set as global setting
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
|
||||
}
|
||||
params = {
|
||||
'marketName': pair.replace('_', '-'),
|
||||
'tickInterval': self.TICKER_INTERVAL,
|
||||
# TODO: Timestamp has no effect on API response
|
||||
'_': minimum_date.timestamp * 1000
|
||||
}
|
||||
data = requests.get(url, params=params, headers=headers).json()
|
||||
if not data['success']:
|
||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||
return data
|
||||
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']:
|
||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||
|
||||
def get_open_orders(self, pair: str) -> List[dict]:
|
||||
data = _API.get_open_orders(pair.replace('_', '-'))
|
||||
if not data['success']:
|
||||
raise RuntimeError('{}: {}'.format(self.name.upper(), 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']]
|
||||
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('_', '-'))
|
||||
@@ -116,5 +202,25 @@ class Bittrex(Exchange):
|
||||
def get_markets(self) -> List[str]:
|
||||
data = _API.get_markets()
|
||||
if not data['success']:
|
||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||
Bittrex._validate_response(data)
|
||||
raise OperationalException('{message}'.format(message=data['message']))
|
||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
||||
|
||||
def get_market_summaries(self) -> List[Dict]:
|
||||
data = _API.get_market_summaries()
|
||||
if not data['success']:
|
||||
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']]
|
||||
|
@@ -1,7 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional
|
||||
|
||||
import arrow
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
|
||||
class Exchange(ABC):
|
||||
@@ -14,11 +12,10 @@ class Exchange(ABC):
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def sleep_time(self) -> float:
|
||||
def fee(self) -> float:
|
||||
"""
|
||||
Sleep time in seconds for the main loop to avoid API rate limits.
|
||||
:return: float
|
||||
Fee for placing an order
|
||||
:return: percentage in float
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@@ -50,10 +47,26 @@ class Exchange(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_ticker(self, pair: str) -> dict:
|
||||
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,
|
||||
@@ -62,15 +75,12 @@ class Exchange(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None) -> dict:
|
||||
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 minimum_date: Minimum date (optional)
|
||||
:return: dict, format: {
|
||||
'success': bool,
|
||||
'message': str,
|
||||
'result': [
|
||||
:param tick_interval: ticker interval in minutes
|
||||
:return: list, format: [
|
||||
{
|
||||
'O': float, (Open)
|
||||
'H': float, (High)
|
||||
@@ -82,6 +92,21 @@ class Exchange(ABC):
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
|
||||
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
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -93,24 +118,6 @@ class Exchange(ABC):
|
||||
:return: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_open_orders(self, pair: str) -> List[dict]:
|
||||
"""
|
||||
Gets all open orders for given pair.
|
||||
:param pair: Pair as str, format: BTC_ETC
|
||||
:return: List of dicts, format: [
|
||||
{
|
||||
'id': str,
|
||||
'type': str,
|
||||
'opened': datetime,
|
||||
'rate': float,
|
||||
'amount': float,
|
||||
'remaining': int,
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_pair_detail_url(self, pair: str) -> str:
|
||||
"""
|
||||
@@ -125,3 +132,41 @@ class Exchange(ABC):
|
||||
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
163
freqtrade/fiat_convert.py
Normal 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
|
@@ -1,146 +1,272 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from jsonschema import validate
|
||||
import arrow
|
||||
import requests
|
||||
from cachetools import cached, TTLCache
|
||||
|
||||
from freqtrade import __version__, exchange, persistence
|
||||
from freqtrade.analyze import get_buy_signal
|
||||
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state
|
||||
from freqtrade import (DependencyException, OperationalException, __version__,
|
||||
exchange, persistence, rpc)
|
||||
from freqtrade.analyze import SignalType, get_signal
|
||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.misc import (State, get_state, load_config, parse_args,
|
||||
throttle, update_state)
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import telegram
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = logging.getLogger('freqtrade')
|
||||
|
||||
_CONF = {}
|
||||
|
||||
|
||||
def _process() -> None:
|
||||
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.
|
||||
:return: None
|
||||
: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
|
||||
trade = create_trade(float(_CONF['stake_amount']))
|
||||
if trade:
|
||||
Trade.session.add(trade)
|
||||
else:
|
||||
logging.info('Got no buy signal...')
|
||||
except ValueError:
|
||||
logger.exception('Unable to create 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:
|
||||
# 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):
|
||||
# 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
|
||||
handle_trade(trade)
|
||||
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 (ConnectionError, json.JSONDecodeError) as error:
|
||||
msg = 'Got {} in _process()'.format(error.__class__.__name__)
|
||||
logger.exception(msg)
|
||||
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 close_trade_if_fulfilled(trade: Trade) -> bool:
|
||||
def check_handle_timedout(timeoutvalue: int) -> None:
|
||||
"""
|
||||
Checks if the trade is closable, and if so it is being closed.
|
||||
:param trade: Trade
|
||||
:return: True if trade has been closed else False
|
||||
"""
|
||||
# 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
|
||||
Check if any orders are timed out and cancel if neccessary
|
||||
:param timeoutvalue: Number of minutes until order is considered timed out
|
||||
: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,
|
||||
trade.pair.replace('_', '/'),
|
||||
exchange.get_pair_detail_url(trade.pair),
|
||||
trade.close_rate,
|
||||
round(profit, 2)
|
||||
)
|
||||
logger.info(message)
|
||||
telegram.send_msg(message)
|
||||
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 should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
||||
def execute_sell(trade: Trade, limit: float) -> None:
|
||||
"""
|
||||
Based an earlier trade and current price and configuration, decides whether bot should sell
|
||||
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 = (current_rate - trade.open_rate) / trade.open_rate
|
||||
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
|
||||
logger.debug('Stop loss hit.')
|
||||
return True
|
||||
|
||||
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
||||
duration, threshold = float(duration), float(threshold)
|
||||
# Check if time matches and current rate is above threshold
|
||||
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
||||
if time_diff > duration and current_profit > threshold:
|
||||
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
||||
if time_diff > float(duration) and current_profit > threshold:
|
||||
return True
|
||||
|
||||
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0)
|
||||
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', float(current_profit) * 100.0)
|
||||
return False
|
||||
|
||||
|
||||
def handle_trade(trade: Trade) -> None:
|
||||
def handle_trade(trade: Trade) -> bool:
|
||||
"""
|
||||
Sells the current pair if the threshold is reached and updates the trade record.
|
||||
:return: None
|
||||
:return: True if trade has been sold, False otherwise
|
||||
"""
|
||||
try:
|
||||
if not trade.is_open:
|
||||
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
||||
|
||||
logger.debug('Handling open trade %s ...', trade)
|
||||
|
||||
logger.debug('Handling %s ...', trade)
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
if should_sell(trade, current_rate, datetime.utcnow()):
|
||||
execute_sell(trade, current_rate)
|
||||
return
|
||||
|
||||
except ValueError:
|
||||
logger.exception('Unable to handle open order')
|
||||
# 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:
|
||||
@@ -151,18 +277,22 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
|
||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||
|
||||
|
||||
def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
def create_trade(stake_amount: float) -> bool:
|
||||
"""
|
||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||
if one pair triggers the buy_signal a new trade record gets created
|
||||
:param stake_amount: amount of btc to spend
|
||||
:return: True if a trade object has been created and persisted, False otherwise
|
||||
"""
|
||||
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
|
||||
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 ValueError(
|
||||
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency'])
|
||||
raise DependencyException(
|
||||
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
||||
)
|
||||
|
||||
# Remove currently opened and latest pairs from whitelist
|
||||
@@ -171,37 +301,51 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
whitelist.remove(trade.pair)
|
||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||
if not whitelist:
|
||||
raise ValueError('No pair in whitelist')
|
||||
raise DependencyException('No pair in whitelist')
|
||||
|
||||
# Pick pair based on StochRSI buy signals
|
||||
for _pair in whitelist:
|
||||
if get_buy_signal(_pair):
|
||||
if get_signal(_pair, SignalType.BUY):
|
||||
pair = _pair
|
||||
break
|
||||
else:
|
||||
return None
|
||||
return False
|
||||
|
||||
open_rate = get_target_bid(exchange.get_ticker(pair))
|
||||
amount = stake_amount / open_rate
|
||||
order_id = exchange.buy(pair, open_rate, amount)
|
||||
# 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
|
||||
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
||||
exchange.EXCHANGE.name.upper(),
|
||||
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '.format(
|
||||
exchange.get_name().upper(),
|
||||
pair.replace('_', '/'),
|
||||
exchange.get_pair_detail_url(pair),
|
||||
open_rate
|
||||
)
|
||||
logger.info(message)
|
||||
telegram.send_msg(message)
|
||||
return Trade(pair=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,
|
||||
open_rate=open_rate,
|
||||
open_date=datetime.utcnow(),
|
||||
amount=amount,
|
||||
exchange=exchange.EXCHANGE.name.upper(),
|
||||
open_order_id=order_id,
|
||||
is_open=True)
|
||||
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:
|
||||
@@ -212,7 +356,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
:return: None
|
||||
"""
|
||||
# Initialize all modules
|
||||
telegram.init(config)
|
||||
rpc.init(config)
|
||||
persistence.init(config, db_url)
|
||||
exchange.init(config)
|
||||
|
||||
@@ -224,49 +368,104 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
update_state(State.STOPPED)
|
||||
|
||||
|
||||
def app(config: dict) -> None:
|
||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||
def gen_pair_whitelist(base_currency: str, key: str = 'BaseVolume') -> List[str]:
|
||||
"""
|
||||
Main loop which handles the application state
|
||||
:param config: config as dict
|
||||
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
|
||||
"""
|
||||
logger.info('Starting freqtrade %s', __version__)
|
||||
init(config)
|
||||
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:
|
||||
old_state = get_state()
|
||||
logger.info('Initial State: %s', old_state)
|
||||
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
|
||||
init(_CONF)
|
||||
old_state = None
|
||||
while True:
|
||||
new_state = get_state()
|
||||
# Log state transition
|
||||
if new_state != old_state:
|
||||
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
||||
logging.info('Changing state to: %s', new_state.name)
|
||||
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:
|
||||
_process()
|
||||
# We need to sleep here because otherwise we would run into bittrex rate limit
|
||||
time.sleep(exchange.EXCHANGE.sleep_time)
|
||||
throttle(
|
||||
_process,
|
||||
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
||||
nb_assets=args.dynamic_whitelist,
|
||||
)
|
||||
old_state = new_state
|
||||
except RuntimeError:
|
||||
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
||||
logger.exception('RuntimeError. Trader stopped!')
|
||||
except KeyboardInterrupt:
|
||||
logger.info('Got SIGINT, aborting ...')
|
||||
except BaseException:
|
||||
logger.exception('Got fatal exception!')
|
||||
finally:
|
||||
telegram.send_msg('*Status:* `Trader has stopped`')
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Loads and validates the config and starts the main loop
|
||||
:return: None
|
||||
"""
|
||||
global _CONF
|
||||
with open('config.json') as file:
|
||||
_CONF = json.load(file)
|
||||
validate(_CONF, CONF_SCHEMA)
|
||||
app(_CONF)
|
||||
cleanup()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@@ -1,7 +1,19 @@
|
||||
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
|
||||
@@ -32,6 +44,175 @@ def get_state() -> State:
|
||||
return _STATE
|
||||
|
||||
|
||||
def load_config(path: str) -> Dict:
|
||||
"""
|
||||
Loads a config file from the given path
|
||||
:param path: path as str
|
||||
:return: configuration as dictionary
|
||||
"""
|
||||
with open(path) as file:
|
||||
conf = json.load(file)
|
||||
if 'internals' not in conf:
|
||||
conf['internals'] = {}
|
||||
logger.info('Validating configuration ...')
|
||||
try:
|
||||
validate(conf, CONF_SCHEMA)
|
||||
return conf
|
||||
except ValidationError as exception:
|
||||
logger.fatal('Invalid configuration. See config.json.example. Reason: %s', exception)
|
||||
raise ValidationError(
|
||||
best_match(Draft4Validator(CONF_SCHEMA).iter_errors(conf)).message
|
||||
)
|
||||
|
||||
|
||||
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||
"""
|
||||
Throttles the given callable that it
|
||||
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',
|
||||
@@ -39,6 +220,14 @@ CONF_SCHEMA = {
|
||||
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
||||
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
||||
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
|
||||
'CLP', 'CNY', 'CZK', 'DKK',
|
||||
'EUR', 'GBP', 'HKD', 'HUF',
|
||||
'IDR', 'ILS', 'INR', 'JPY',
|
||||
'KRW', 'MXN', 'MYR', 'NOK',
|
||||
'NZD', 'PHP', 'PKR', 'PLN',
|
||||
'RUB', 'SEK', 'SGD', 'THB',
|
||||
'TRY', 'TWD', 'ZAR', 'USD']},
|
||||
'dry_run': {'type': 'boolean'},
|
||||
'minimal_roi': {
|
||||
'type': 'object',
|
||||
@@ -48,6 +237,7 @@ CONF_SCHEMA = {
|
||||
'minProperties': 1
|
||||
},
|
||||
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
|
||||
'unfilledtimeout': {'type': 'integer', 'minimum': 0},
|
||||
'bid_strategy': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
@@ -61,6 +251,13 @@ CONF_SCHEMA = {
|
||||
'required': ['ask_last_balance']
|
||||
},
|
||||
'exchange': {'$ref': '#/definitions/exchange'},
|
||||
'experimental': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'use_sell_signal': {'type': 'boolean'},
|
||||
'sell_profit_only': {'type': 'boolean'}
|
||||
}
|
||||
},
|
||||
'telegram': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
@@ -71,6 +268,12 @@ CONF_SCHEMA = {
|
||||
'required': ['enabled', 'token', 'chat_id']
|
||||
},
|
||||
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
|
||||
'internals': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'process_throttle_secs': {'type': 'number'}
|
||||
}
|
||||
}
|
||||
},
|
||||
'definitions': {
|
||||
'exchange': {
|
||||
@@ -81,7 +284,18 @@ CONF_SCHEMA = {
|
||||
'secret': {'type': 'string'},
|
||||
'pair_whitelist': {
|
||||
'type': 'array',
|
||||
'items': {'type': 'string'},
|
||||
'items': {
|
||||
'type': 'string',
|
||||
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||
},
|
||||
'uniqueItems': True
|
||||
},
|
||||
'pair_blacklist': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'string',
|
||||
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||
},
|
||||
'uniqueItems': True
|
||||
}
|
||||
},
|
||||
@@ -95,6 +309,7 @@ CONF_SCHEMA = {
|
||||
'max_open_trades',
|
||||
'stake_currency',
|
||||
'stake_amount',
|
||||
'fiat_display_currency',
|
||||
'dry_run',
|
||||
'minimal_roi',
|
||||
'bid_strategy',
|
||||
|
133
freqtrade/optimize/__init__.py
Normal file
133
freqtrade/optimize/__init__.py
Normal 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
|
197
freqtrade/optimize/backtesting.py
Normal file
197
freqtrade/optimize/backtesting.py
Normal 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)
|
||||
)
|
318
freqtrade/optimize/hyperopt.py
Normal file
318
freqtrade/optimize/hyperopt.py
Normal 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)
|
41
freqtrade/optimize/hyperopt_conf.py
Normal file
41
freqtrade/optimize/hyperopt_conf.py
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
@@ -1,87 +1,198 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from decimal import Decimal, getcontext
|
||||
from typing import Dict, Optional
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
|
||||
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.types import Enum
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from freqtrade import exchange
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CONF = {}
|
||||
|
||||
Base = declarative_base()
|
||||
_DECL_BASE = declarative_base()
|
||||
|
||||
|
||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
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 db_url: database connector string for sqlalchemy (Optional)
|
||||
:param engine: database engine for sqlalchemy (Optional)
|
||||
:return: None
|
||||
"""
|
||||
_CONF.update(config)
|
||||
if not db_url:
|
||||
if not engine:
|
||||
if _CONF.get('dry_run', False):
|
||||
db_url = 'sqlite:///tradesv2.dry_run.sqlite'
|
||||
# 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:
|
||||
db_url = 'sqlite:///tradesv2.sqlite'
|
||||
engine = create_engine('sqlite://',
|
||||
connect_args={'check_same_thread': False},
|
||||
poolclass=StaticPool,
|
||||
echo=False)
|
||||
else:
|
||||
engine = create_engine('sqlite:///tradesv3.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)
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
|
||||
|
||||
class Trade(Base):
|
||||
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)
|
||||
open_rate = Column(Float, nullable=False)
|
||||
fee = Column(Float, nullable=False, default=0.0)
|
||||
open_rate = Column(Float)
|
||||
close_rate = Column(Float)
|
||||
close_profit = Column(Float)
|
||||
stake_amount = Column(Float, name='btc_amount', nullable=False)
|
||||
amount = Column(Float, nullable=False)
|
||||
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):
|
||||
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(
|
||||
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
|
||||
self.id,
|
||||
self.pair,
|
||||
self.amount,
|
||||
self.open_rate,
|
||||
open_since
|
||||
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||
)
|
||||
|
||||
def exec_sell_order(self, rate: float, amount: float) -> float:
|
||||
def update(self, order: Dict) -> None:
|
||||
"""
|
||||
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
|
||||
Updates this entity with amount and actual open/close rates.
|
||||
:param order: order retrieved by exchange.get_order()
|
||||
:return: None
|
||||
"""
|
||||
profit = 100 * ((rate - self.open_rate) / self.open_rate)
|
||||
# Ignore open and cancelled orders
|
||||
if not order['closed'] or order['rate'] is None:
|
||||
return
|
||||
|
||||
# Execute sell and update trade record
|
||||
order_id = exchange.sell(str(self.pair), rate, amount)
|
||||
self.close_rate = rate
|
||||
self.close_profit = profit
|
||||
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.open_order_id = order_id
|
||||
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
|
||||
)
|
||||
|
||||
# Flush changes
|
||||
Trade.session.flush()
|
||||
return profit
|
||||
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))
|
||||
|
@@ -1 +1,42 @@
|
||||
import logging
|
||||
|
||||
from . import telegram
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
REGISTERED_MODULES = []
|
||||
|
||||
|
||||
def init(config: dict) -> None:
|
||||
"""
|
||||
Initializes all enabled rpc modules
|
||||
:param config: config to use
|
||||
:return: None
|
||||
"""
|
||||
|
||||
if config['telegram'].get('enabled', False):
|
||||
logger.info('Enabling rpc.telegram ...')
|
||||
REGISTERED_MODULES.append('telegram')
|
||||
telegram.init(config)
|
||||
|
||||
|
||||
def cleanup() -> None:
|
||||
"""
|
||||
Stops all enabled rpc modules
|
||||
:return: None
|
||||
"""
|
||||
if 'telegram' in REGISTERED_MODULES:
|
||||
logger.debug('Cleaning up rpc.telegram ...')
|
||||
telegram.cleanup()
|
||||
|
||||
|
||||
def send_msg(msg: str) -> None:
|
||||
"""
|
||||
Send given markdown message to all registered rpc modules
|
||||
:param msg: message
|
||||
:return: None
|
||||
"""
|
||||
logger.info(msg)
|
||||
if 'telegram' in REGISTERED_MODULES:
|
||||
telegram.send_msg(msg)
|
||||
|
@@ -1,15 +1,20 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Callable, Any
|
||||
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 telegram import ParseMode, Bot, Update
|
||||
from telegram.error import NetworkError
|
||||
from tabulate import tabulate
|
||||
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update
|
||||
from telegram.error import NetworkError, TelegramError
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.misc import get_state, State, update_state
|
||||
from freqtrade import __version__, exchange
|
||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.misc import State, get_state, update_state
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
# Remove noisy log messages
|
||||
@@ -17,8 +22,9 @@ logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_updater = None
|
||||
_UPDATER: Updater = None
|
||||
_CONF = {}
|
||||
_FIAT_CONVERT = CryptoToFiatConverter()
|
||||
|
||||
|
||||
def init(config: dict) -> None:
|
||||
@@ -29,28 +35,33 @@ def init(config: dict) -> None:
|
||||
:param config: config to use
|
||||
:return: None
|
||||
"""
|
||||
global _updater
|
||||
global _UPDATER
|
||||
|
||||
_CONF.update(config)
|
||||
if not _CONF['telegram']['enabled']:
|
||||
if not is_enabled():
|
||||
return
|
||||
|
||||
_updater = Updater(token=config['telegram']['token'], workers=0)
|
||||
_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(
|
||||
_UPDATER.dispatcher.add_handler(handle)
|
||||
_UPDATER.start_polling(
|
||||
clean=True,
|
||||
bootstrap_retries=3,
|
||||
bootstrap_retries=-1,
|
||||
timeout=30,
|
||||
read_latency=60,
|
||||
)
|
||||
@@ -60,6 +71,23 @@ def init(config: dict) -> None:
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -67,19 +95,21 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
|
||||
:return: decorated function
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
bot, update = kwargs.get('bot') or args[0], kwargs.get('update') or args[1]
|
||||
|
||||
if not isinstance(bot, Bot) or not isinstance(update, Update):
|
||||
raise ValueError('Received invalid Arguments: {}'.format(*args))
|
||||
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('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
|
||||
return command_handler(*args, **kwargs)
|
||||
else:
|
||||
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:
|
||||
@@ -90,32 +120,39 @@ def _status(bot: Bot, update: Update) -> None:
|
||||
: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 order`', bot=bot)
|
||||
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)['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
|
||||
|
||||
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, 2)
|
||||
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}`
|
||||
*Open Rate:* `{open_rate:.8f}`
|
||||
*Close Rate:* `{close_rate}`
|
||||
*Current Rate:* `{current_rate}`
|
||||
*Current Rate:* `{current_rate:.8f}`
|
||||
*Close Profit:* `{close_profit}`
|
||||
*Current Profit:* `{current_profit:.2f}%`
|
||||
*Open Order:* `{open_order}`
|
||||
@@ -129,12 +166,110 @@ def _status(bot: Bot, update: Update) -> None:
|
||||
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,
|
||||
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:
|
||||
"""
|
||||
@@ -146,21 +281,31 @@ def _profit(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
trades = Trade.query.order_by(Trade.id).all()
|
||||
|
||||
profit_amounts = []
|
||||
profits = []
|
||||
profit_all_coin = []
|
||||
profit_all_percent = []
|
||||
profit_closed_coin = []
|
||||
profit_closed_percent = []
|
||||
durations = []
|
||||
|
||||
for trade in trades:
|
||||
current_rate = None
|
||||
|
||||
if not trade.open_rate:
|
||||
continue
|
||||
if trade.close_date:
|
||||
durations.append((trade.close_date - trade.open_date).total_seconds())
|
||||
if trade.close_profit:
|
||||
profit = trade.close_profit
|
||||
|
||||
if not trade.is_open:
|
||||
profit_percent = trade.calc_profit_percent()
|
||||
profit_closed_coin.append(trade.calc_profit())
|
||||
profit_closed_percent.append(profit_percent)
|
||||
else:
|
||||
# Get current rate
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
|
||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
||||
|
||||
profit_amounts.append((profit / 100) * trade.stake_amount)
|
||||
profits.append(profit)
|
||||
profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
|
||||
profit_all_percent.append(profit_percent)
|
||||
|
||||
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
||||
.filter(Trade.is_open.is_(False)) \
|
||||
@@ -173,26 +318,80 @@ def _profit(bot: Bot, update: Update) -> None:
|
||||
return
|
||||
|
||||
bp_pair, bp_rate = best_pair
|
||||
|
||||
# Prepare data to display
|
||||
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
||||
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
|
||||
profit_closed_fiat = _FIAT_CONVERT.convert_amount(
|
||||
profit_closed_coin,
|
||||
_CONF['stake_currency'],
|
||||
_CONF['fiat_display_currency']
|
||||
)
|
||||
profit_all_coin = round(sum(profit_all_coin), 8)
|
||||
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
|
||||
profit_all_fiat = _FIAT_CONVERT.convert_amount(
|
||||
profit_all_coin,
|
||||
_CONF['stake_currency'],
|
||||
_CONF['fiat_display_currency']
|
||||
)
|
||||
|
||||
# Message to display
|
||||
markdown_msg = """
|
||||
*ROI:* `{profit_btc:.2f} ({profit:.2f}%)`
|
||||
*Trade Count:* `{trade_count}`
|
||||
*ROI:* Close trades
|
||||
∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`
|
||||
∙ `{profit_closed_fiat:.3f} {fiat}`
|
||||
*ROI:* All trades
|
||||
∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`
|
||||
∙ `{profit_all_fiat:.3f} {fiat}`
|
||||
|
||||
*Total Trade Count:* `{trade_count}`
|
||||
*First Trade opened:* `{first_trade_date}`
|
||||
*Latest Trade opened:* `{latest_trade_date}`
|
||||
*Avg. Duration:* `{avg_duration}`
|
||||
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
|
||||
""".format(
|
||||
profit_btc=round(sum(profit_amounts), 8),
|
||||
profit=round(sum(profits), 2),
|
||||
coin=_CONF['stake_currency'],
|
||||
fiat=_CONF['fiat_display_currency'],
|
||||
profit_closed_coin=profit_closed_coin,
|
||||
profit_closed_percent=profit_closed_percent,
|
||||
profit_closed_fiat=profit_closed_fiat,
|
||||
profit_all_coin=profit_all_coin,
|
||||
profit_all_percent=profit_all_percent,
|
||||
profit_all_fiat=profit_all_fiat,
|
||||
trade_count=len(trades),
|
||||
first_trade_date=arrow.get(trades[0].open_date).humanize(),
|
||||
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
|
||||
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
|
||||
best_pair=bp_pair,
|
||||
best_rate=round(bp_rate, 2),
|
||||
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:
|
||||
"""
|
||||
@@ -237,38 +436,24 @@ def _forcesell(bot: Bot, update: Update) -> None:
|
||||
send_msg('`trader is not running`', bot=bot)
|
||||
return
|
||||
|
||||
try:
|
||||
trade_id = int(update.message.text
|
||||
.replace('/forcesell', '')
|
||||
.strip())
|
||||
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('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,
|
||||
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>`')
|
||||
send_msg('Invalid argument. See `/help` to view usage')
|
||||
logger.warning('/forcesell: Invalid argument received')
|
||||
return
|
||||
|
||||
_exec_forcesell(trade)
|
||||
|
||||
|
||||
@authorized_only
|
||||
@@ -284,23 +469,121 @@ def _performance(bot: Bot, update: Update) -> None:
|
||||
send_msg('`trader is not running`', bot=bot)
|
||||
return
|
||||
|
||||
pair_rates = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
||||
pair_rates = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum'),
|
||||
func.count(Trade.pair).label('count')) \
|
||||
.filter(Trade.is_open.is_(False)) \
|
||||
.group_by(Trade.pair) \
|
||||
.order_by(text('profit_sum DESC')) \
|
||||
.all()
|
||||
|
||||
stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format(
|
||||
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
|
||||
index=i + 1,
|
||||
pair=pair,
|
||||
profit=round(rate, 2)
|
||||
) for i, (pair, rate) in enumerate(pair_rates))
|
||||
profit=round(rate * 100, 2),
|
||||
count=count
|
||||
) for i, (pair, rate, count) in enumerate(pair_rates))
|
||||
|
||||
message = '<b>Performance:</b>\n{}\n'.format(stats)
|
||||
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
|
||||
@@ -309,18 +592,33 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
|
||||
:param parse_mode: telegram parse mode
|
||||
:return: None
|
||||
"""
|
||||
if _CONF['telegram'].get('enabled', False):
|
||||
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:
|
||||
bot = bot or _updater.bot
|
||||
try:
|
||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||
except NetworkError as error:
|
||||
bot.send_message(
|
||||
_CONF['telegram']['chat_id'], msg,
|
||||
parse_mode=parse_mode, reply_markup=reply_markup
|
||||
)
|
||||
except NetworkError as network_err:
|
||||
# Sometimes the telegram server resets the current connection,
|
||||
# if this is the case we send the message again.
|
||||
logger.warning(
|
||||
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||
error.message
|
||||
network_err.message
|
||||
)
|
||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||
except Exception:
|
||||
logger.exception('Exception occurred within Telegram API')
|
||||
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)
|
||||
|
218
freqtrade/tests/conftest.py
Normal file
218
freqtrade/tests/conftest.py
Normal 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
|
||||
}
|
||||
]
|
211
freqtrade/tests/exchange/test_exchange.py
Normal file
211
freqtrade/tests/exchange/test_exchange.py
Normal 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
|
337
freqtrade/tests/exchange/test_exchange_bittrex.py
Normal file
337
freqtrade/tests/exchange/test_exchange_bittrex.py
Normal 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)
|
177
freqtrade/tests/optimize/test_backtesting.py
Normal file
177
freqtrade/tests/optimize/test_backtesting.py
Normal 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
|
223
freqtrade/tests/optimize/test_hyperopt.py
Normal file
223
freqtrade/tests/optimize/test_hyperopt.py
Normal 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()
|
16
freqtrade/tests/optimize/test_hyperopt_config.py
Normal file
16
freqtrade/tests/optimize/test_hyperopt_config.py
Normal 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']
|
176
freqtrade/tests/optimize/test_optimize.py
Normal file
176
freqtrade/tests/optimize/test_optimize.py
Normal 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)
|
57
freqtrade/tests/rpc/test_rpc.py
Normal file
57
freqtrade/tests/rpc/test_rpc.py
Normal 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
|
668
freqtrade/tests/rpc/test_rpc_telegram.py
Normal file
668
freqtrade/tests/rpc/test_rpc_telegram.py
Normal 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
|
141
freqtrade/tests/test_acl_pair.py
Normal file
141
freqtrade/tests/test_acl_pair.py
Normal 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)
|
@@ -1,47 +1,74 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import pytest
|
||||
# pragma pylint: disable=missing-docstring,W0621
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
||||
get_buy_signal
|
||||
from freqtrade.analyze import (SignalType, get_signal, parse_ticker_dataframe,
|
||||
populate_buy_trend, populate_indicators,
|
||||
populate_sell_trend)
|
||||
|
||||
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},
|
||||
]
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def result():
|
||||
return parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00'))
|
||||
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||
return parse_ticker_dataframe(json.load(data_file))
|
||||
|
||||
def test_dataframe_has_correct_columns(result):
|
||||
|
||||
def test_dataframe_correct_columns(result):
|
||||
assert result.columns.tolist() == \
|
||||
['close', 'high', 'low', 'open', 'date', 'volume']
|
||||
|
||||
def test_orders_by_date(result):
|
||||
assert 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_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
|
||||
assert 'buy_price' in dataframe.columns
|
||||
|
||||
|
||||
def test_populates_sell_trend(result):
|
||||
dataframe = populate_sell_trend(populate_indicators(result))
|
||||
assert 'sell' in dataframe.columns
|
||||
|
||||
|
||||
def test_returns_latest_buy_signal(mocker):
|
||||
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||
assert get_buy_signal('BTC-ETH')
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.analyze.analyze_ticker',
|
||||
return_value=DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert get_signal('BTC-ETH', SignalType.BUY)
|
||||
|
||||
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||
assert not get_buy_signal('BTC-ETH')
|
||||
mocker.patch(
|
||||
'freqtrade.analyze.analyze_ticker',
|
||||
return_value=DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert not get_signal('BTC-ETH', SignalType.BUY)
|
||||
|
||||
|
||||
def test_returns_latest_sell_signal(mocker):
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||
mocker.patch(
|
||||
'freqtrade.analyze.analyze_ticker',
|
||||
return_value=DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert get_signal('BTC-ETH', SignalType.SELL)
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.analyze.analyze_ticker',
|
||||
return_value=DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
|
||||
)
|
||||
assert not get_signal('BTC-ETH', SignalType.SELL)
|
||||
|
||||
|
||||
def test_get_signal_handles_exceptions(mocker):
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=MagicMock())
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker',
|
||||
side_effect=Exception('invalid ticker history '))
|
||||
|
||||
assert not get_signal('BTC-ETH', SignalType.BUY)
|
||||
|
@@ -1,77 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.analyze import analyze_ticker
|
||||
from freqtrade.main import should_sell
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||
|
||||
def print_results(results):
|
||||
print('Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
||||
len(results.index),
|
||||
results.profit.mean() * 100.0,
|
||||
results.profit.sum(),
|
||||
results.duration.mean()*5
|
||||
))
|
||||
|
||||
@pytest.fixture
|
||||
def pairs():
|
||||
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
||||
|
||||
@pytest.fixture
|
||||
def conf():
|
||||
return {
|
||||
"minimal_roi": {
|
||||
"60": 0.0,
|
||||
"40": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.03
|
||||
},
|
||||
"stoploss": -0.40
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||
def test_backtest(conf, pairs, mocker):
|
||||
trades = []
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
for pair in pairs:
|
||||
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
||||
data = json.load(data_file)
|
||||
|
||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data)
|
||||
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
||||
ticker = analyze_ticker(pair)
|
||||
# for each buy point
|
||||
for index, row in ticker[ticker.buy == 1].iterrows():
|
||||
trade = Trade(
|
||||
open_rate=row['close'],
|
||||
open_date=arrow.get(row['date']).datetime,
|
||||
amount=1,
|
||||
)
|
||||
# calculate win/lose forwards from buy point
|
||||
for index2, row2 in ticker[index:].iterrows():
|
||||
if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime):
|
||||
current_profit = (row2['close'] - trade.open_rate) / trade.open_rate
|
||||
|
||||
trades.append((pair, current_profit, index2 - index))
|
||||
break
|
||||
|
||||
labels = ['currency', 'profit', 'duration']
|
||||
results = DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
print('====================== BACKTESTING REPORT ================================')
|
||||
|
||||
for pair in pairs:
|
||||
print('For currency {}:'.format(pair))
|
||||
print_results(results[results.currency == pair])
|
||||
print('TOTAL OVER ALL TRADES:')
|
||||
print_results(results)
|
27
freqtrade/tests/test_dataframe.py
Normal file
27
freqtrade/tests/test_dataframe.py
Normal 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
|
125
freqtrade/tests/test_fiat_convert.py
Normal file
125
freqtrade/tests/test_fiat_convert.py
Normal 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
|
@@ -1,126 +1,676 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
# pragma pylint: disable=missing-docstring,C0103
|
||||
import copy
|
||||
from unittest.mock import MagicMock, call
|
||||
import logging
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from jsonschema import validate
|
||||
import requests
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
import freqtrade.main as main
|
||||
from freqtrade import DependencyException, OperationalException
|
||||
from freqtrade.analyze import SignalType
|
||||
from freqtrade.exchange import Exchanges
|
||||
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
||||
get_target_bid
|
||||
from freqtrade.misc import CONF_SCHEMA
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conf():
|
||||
configuration = {
|
||||
"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
|
||||
},
|
||||
"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",
|
||||
]
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": True,
|
||||
"token": "token",
|
||||
"chat_id": "chat_id"
|
||||
}
|
||||
}
|
||||
validate(configuration, CONF_SCHEMA)
|
||||
return configuration
|
||||
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_create_trade(conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
|
||||
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=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
# Save state of current whitelist
|
||||
whitelist = copy.deepcopy(conf['exchange']['pair_whitelist'])
|
||||
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://'))
|
||||
|
||||
init(conf, 'sqlite://')
|
||||
for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']:
|
||||
trade = create_trade(15.0)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
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.open_rate == 0.072661
|
||||
assert trade.pair == pair
|
||||
assert trade.exchange == Exchanges.BITTREX.name
|
||||
assert trade.amount == 206.43811673387373
|
||||
assert trade.stake_amount == 15.0
|
||||
assert trade.stake_amount == default_conf['stake_amount']
|
||||
assert trade.is_open
|
||||
assert trade.open_date is not None
|
||||
assert whitelist == conf['exchange']['pair_whitelist']
|
||||
assert trade.exchange == Exchanges.BITTREX.name
|
||||
assert trade.open_rate == 0.00001099
|
||||
assert trade.amount == 90.99181073703367
|
||||
|
||||
buy_signal.assert_has_calls(
|
||||
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]
|
||||
|
||||
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_handle_trade(conf, mocker):
|
||||
|
||||
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)
|
||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
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.17256061,
|
||||
'ask': 0.172661,
|
||||
'last': 0.17256061
|
||||
'bid': 0.00001172,
|
||||
'ask': 0.00001173,
|
||||
'last': 0.00001172
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||
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.close_rate == 0.17256061
|
||||
assert trade.close_profit == 137.4872490056564
|
||||
assert trade.close_date is not None
|
||||
assert trade.open_order_id == 'dry_run'
|
||||
assert trade.open_order_id == 'mocked_limit_sell'
|
||||
|
||||
def test_close_trade(conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||
# 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
|
||||
|
||||
# Simulate that there is no open order
|
||||
trade.open_order_id = None
|
||||
trade.update(limit_buy_order)
|
||||
trade.update(limit_sell_order)
|
||||
assert trade.is_open is False
|
||||
|
||||
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
|
||||
|
||||
closed = close_trade_if_fulfilled(trade)
|
||||
assert closed
|
||||
assert not trade.is_open
|
||||
|
||||
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_when_last_bigger_than_ask(mocker):
|
||||
|
||||
def test_balance_bigger_last_ask(mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
||||
|
||||
|
||||
def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Increase the price and sell it
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker_sell_up)
|
||||
|
||||
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Decrease the price and sell it
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker_sell_down)
|
||||
|
||||
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
|
||||
# Create some test data
|
||||
create_trade(0.001)
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
|
||||
# Increase the price and sell it
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker_sell_up)
|
||||
mocker.patch('freqtrade.main._CONF', {})
|
||||
|
||||
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
||||
|
||||
assert rpc_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '0.00001172' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0]
|
||||
assert 'USD' not in rpc_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker):
|
||||
default_conf['experimental'] = {
|
||||
'use_sell_signal': True,
|
||||
'sell_profit_only': True,
|
||||
}
|
||||
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.00002172,
|
||||
'ask': 0.00002173,
|
||||
'last': 0.00002172
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.update(limit_buy_order)
|
||||
assert handle_trade(trade) is True
|
||||
|
||||
|
||||
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker):
|
||||
default_conf['experimental'] = {
|
||||
'use_sell_signal': True,
|
||||
'sell_profit_only': False,
|
||||
}
|
||||
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.00002172,
|
||||
'ask': 0.00002173,
|
||||
'last': 0.00002172
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.update(limit_buy_order)
|
||||
assert handle_trade(trade) is True
|
||||
|
||||
|
||||
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker):
|
||||
default_conf['experimental'] = {
|
||||
'use_sell_signal': True,
|
||||
'sell_profit_only': True,
|
||||
}
|
||||
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.00000172,
|
||||
'ask': 0.00000173,
|
||||
'last': 0.00000172
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.update(limit_buy_order)
|
||||
assert handle_trade(trade) is False
|
||||
|
||||
|
||||
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker):
|
||||
default_conf['experimental'] = {
|
||||
'use_sell_signal': True,
|
||||
'sell_profit_only': False,
|
||||
}
|
||||
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.00000172,
|
||||
'ask': 0.00000173,
|
||||
'last': 0.00000172
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
init(default_conf, create_engine('sqlite://'))
|
||||
create_trade(0.001)
|
||||
|
||||
trade = Trade.query.first()
|
||||
trade.update(limit_buy_order)
|
||||
assert handle_trade(trade) is True
|
||||
|
164
freqtrade/tests/test_misc.py
Normal file
164
freqtrade/tests/test_misc.py
Normal 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')
|
@@ -1,20 +1,312 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
from freqtrade.exchange import Exchanges
|
||||
from freqtrade.persistence import Trade
|
||||
import os
|
||||
|
||||
def test_exec_sell_order(mocker):
|
||||
api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id')
|
||||
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,
|
||||
open_rate=0.50,
|
||||
amount=10.00,
|
||||
fee=0.1,
|
||||
exchange=Exchanges.BITTREX,
|
||||
open_order_id='mocked'
|
||||
)
|
||||
profit = trade.exec_sell_order(1.00, 10.00)
|
||||
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0)
|
||||
assert profit == 100.0
|
||||
assert trade.close_rate == 1.0
|
||||
assert trade.close_profit == profit
|
||||
assert trade.close_date is not None
|
||||
|
||||
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
|
||||
|
@@ -1,199 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from jsonschema import validate
|
||||
from telegram import Bot, Update, Message, Chat
|
||||
|
||||
from freqtrade.main import init, create_trade
|
||||
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conf():
|
||||
configuration = {
|
||||
"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
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
"enabled": True,
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"pair_whitelist": [
|
||||
"BTC_ETH"
|
||||
]
|
||||
},
|
||||
"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
|
||||
|
||||
|
||||
class MagicBot(MagicMock, Bot):
|
||||
pass
|
||||
|
||||
|
||||
def test_status_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_status(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 2
|
||||
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
def test_profit_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
trade.close_rate = 0.07256061
|
||||
trade.close_profit = 100.00
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.open_order_id = None
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_profit(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 2
|
||||
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
def test_forcesell_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
update.message.text = '/forcesell 1'
|
||||
_forcesell(bot=MagicBot(), update=update)
|
||||
|
||||
assert msg_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||
assert '0.072561' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
def test_performance_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
trade.close_rate = 0.07256061
|
||||
trade.close_profit = 100.00
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.open_order_id = None
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_performance(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 2
|
||||
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
|
||||
assert 'BTC_ETH 100.00%' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
def test_start_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
update_state(State.STOPPED)
|
||||
assert get_state() == State.STOPPED
|
||||
_start(bot=MagicBot(), update=update)
|
||||
assert get_state() == State.RUNNING
|
||||
assert msg_mock.call_count == 0
|
||||
|
||||
def test_stop_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
update_state(State.RUNNING)
|
||||
assert get_state() == State.RUNNING
|
||||
_stop(bot=MagicBot(), 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]
|
1
freqtrade/tests/testdata/BTC_ADA-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ADA-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ADA-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ADA-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LTC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LTC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LTC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LTC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_NXT-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_NXT-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_NXT-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_NXT-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_UNITEST-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_UNITEST-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XMR-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XMR-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XMR-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XMR-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ZEC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ZEC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ZEC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ZEC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-edg.json
vendored
1
freqtrade/tests/testdata/btc-edg.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-etc.json
vendored
1
freqtrade/tests/testdata/btc-etc.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-eth.json
vendored
1
freqtrade/tests/testdata/btc-eth.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-ltc.json
vendored
1
freqtrade/tests/testdata/btc-ltc.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-mtl.json
vendored
1
freqtrade/tests/testdata/btc-mtl.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-neo.json
vendored
1
freqtrade/tests/testdata/btc-neo.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-omg.json
vendored
1
freqtrade/tests/testdata/btc-omg.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pay.json
vendored
1
freqtrade/tests/testdata/btc-pay.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pivx.json
vendored
1
freqtrade/tests/testdata/btc-pivx.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-qtum.json
vendored
1
freqtrade/tests/testdata/btc-qtum.json
vendored
File diff suppressed because one or more lines are too long
29
freqtrade/tests/testdata/download_backtest_data.py
vendored
Executable file
29
freqtrade/tests/testdata/download_backtest_data.py
vendored
Executable 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
0
freqtrade/vendor/__init__.py
vendored
Normal file
0
freqtrade/vendor/qtpylib/__init__.py
vendored
Normal file
0
freqtrade/vendor/qtpylib/__init__.py
vendored
Normal file
625
freqtrade/vendor/qtpylib/indicators.py
vendored
Normal file
625
freqtrade/vendor/qtpylib/indicators.py
vendored
Normal 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
8
install_ta-lib.sh
Executable 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
|
@@ -1,20 +1,26 @@
|
||||
-e git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex
|
||||
SQLAlchemy==1.1.13
|
||||
python-telegram-bot==8.0
|
||||
arrow==0.10.0
|
||||
python-bittrex==0.2.2
|
||||
SQLAlchemy==1.2.0
|
||||
python-telegram-bot==9.0.0
|
||||
arrow==0.12.0
|
||||
cachetools==2.0.1
|
||||
requests==2.18.4
|
||||
urllib3==1.22
|
||||
wrapt==1.10.11
|
||||
pandas==0.20.3
|
||||
scikit-learn==0.19.0
|
||||
scipy==0.19.1
|
||||
pandas==0.22.0
|
||||
scikit-learn==0.19.1
|
||||
scipy==1.0.0
|
||||
jsonschema==2.6.0
|
||||
numpy==1.13.3
|
||||
TA-Lib==0.4.10
|
||||
pytest==3.2.2
|
||||
numpy==1.14.0
|
||||
TA-Lib==0.4.15
|
||||
pytest==3.3.2
|
||||
pytest-mock==1.6.3
|
||||
pytest-cov==2.5.1
|
||||
hyperopt==0.1
|
||||
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
||||
networkx==1.11
|
||||
tabulate==0.8.2
|
||||
pymarketcap==3.3.147
|
||||
|
||||
# Required for plotting data
|
||||
#matplotlib==2.0.2
|
||||
#matplotlib==2.1.0
|
||||
#PYQT5==5.9
|
70
scripts/plot_dataframe.py
Executable file
70
scripts/plot_dataframe.py
Executable 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(description='Graph utility')
|
||||
parser.add_argument(
|
||||
'-p', '--pair',
|
||||
help = 'What currency pair',
|
||||
dest = 'pair',
|
||||
default = 'BTC_ETH',
|
||||
type = str,
|
||||
)
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
def plot_analyzed_dataframe(args) -> None:
|
||||
"""
|
||||
Calls analyze() and plots the returned dataframe
|
||||
:param pair: pair as str
|
||||
:return: None
|
||||
"""
|
||||
pair = args.pair
|
||||
|
||||
# Init Bittrex to use public API
|
||||
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
||||
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)
|
27
scripts/start-hyperopt-worker.py
Executable file
27
scripts/start-hyperopt-worker.py
Executable 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
21
scripts/start-mongodb.py
Executable 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()
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user