mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-22 12:40:30 +08:00
Compare commits
743
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4faf9158d | ||
|
|
dfb1bf51bd | ||
|
|
f946062265 | ||
|
|
65c6322ba5 | ||
|
|
4a2d5678ad | ||
|
|
06399caa2b | ||
|
|
4c84ea8efc | ||
|
|
c85e698ee2 | ||
|
|
423e30da1e | ||
|
|
c29b1ef3c1 | ||
|
|
17f9906df4 | ||
|
|
41b5135ed4 | ||
|
|
af85ee31c9 | ||
|
|
132dffd239 | ||
|
|
887a7cc825 | ||
|
|
22249506e6 | ||
|
|
b979ffd123 | ||
|
|
0db9950347 | ||
|
|
7c4467d800 | ||
|
|
30dbeaa5fb | ||
|
|
4d6837b5d6 | ||
|
|
098a4c4fc6 | ||
|
|
f5cb6e38d6 | ||
|
|
1bd65397b6 | ||
|
|
c768b207bc | ||
|
|
e18686d5c5 | ||
|
|
b7779cf363 | ||
|
|
28819b8a32 | ||
|
|
a56d7f34c7 | ||
|
|
9f0b3303f1 | ||
|
|
9d7a35658b | ||
|
|
c58cebd1eb | ||
|
|
027cdba474 | ||
|
|
d223529100 | ||
|
|
1e02506ab4 | ||
|
|
2a97ade68e | ||
|
|
9648767e9a | ||
|
|
98449b2088 | ||
|
|
91d16aba3b | ||
|
|
9eb649371b | ||
|
|
7f2ded65bc | ||
|
|
b76b4458cb | ||
|
|
decbdbf6ea | ||
|
|
685ce25b85 | ||
|
|
1cafcc1417 | ||
|
|
0d77854782 | ||
|
|
4cb8d54d97 | ||
|
|
7b796a4276 | ||
|
|
41a4c7072f | ||
|
|
11302b3af9 | ||
|
|
5bb7eed072 | ||
|
|
dbf3b6e6b2 | ||
|
|
3e69449a6b | ||
|
|
69731b653d | ||
|
|
127d779eb1 | ||
|
|
8d86a5548f | ||
|
|
0a37cdec5b | ||
|
|
8f7d678170 | ||
|
|
1de084a08f | ||
|
|
93ca4e990d | ||
|
|
4694372496 | ||
|
|
f0606b5ea4 | ||
|
|
b0dce13672 | ||
|
|
fc9837b678 | ||
|
|
5de89549ee | ||
|
|
586d7f2954 | ||
|
|
218dc0bafd | ||
|
|
89c080fce7 | ||
|
|
4a794aa035 | ||
|
|
cb4668f093 | ||
|
|
6092471180 | ||
|
|
09068a4c37 | ||
|
|
ed406a30ff | ||
|
|
aa520d5a8b | ||
|
|
e59f46dfd6 | ||
|
|
5197ab6cc2 | ||
|
|
d7b6cb8490 | ||
|
|
fa0e9332bf | ||
|
|
d9d6a4e52d | ||
|
|
b4e5b699bd | ||
|
|
f990ecf14d | ||
|
|
5d3a1c2f8b | ||
|
|
3159ec7dc2 | ||
|
|
8be0626fc9 | ||
|
|
868f17fd9d | ||
|
|
b95cf465fc | ||
|
|
c4b10bae39 | ||
|
|
6c4f7afaea | ||
|
|
b85219d5b4 | ||
|
|
082b342d02 | ||
|
|
37c1057ab6 | ||
|
|
46e1a87a3d | ||
|
|
cfafafb8fc | ||
|
|
497212383a | ||
|
|
e5870ea60a | ||
|
|
8587fee0ce | ||
|
|
388535b09c | ||
|
|
25e9f0f58f | ||
|
|
b4bd557273 | ||
|
|
2577b53518 | ||
|
|
8fe3ab344e | ||
|
|
bfd7e4b2dd | ||
|
|
50310576f9 | ||
|
|
fea2ed104e | ||
|
|
127878413e | ||
|
|
6a6ccf5595 | ||
|
|
d40585f56e | ||
|
|
4337abd60a | ||
|
|
92e0a7bb88 | ||
|
|
20f8a75f4a | ||
|
|
634d22fb06 | ||
|
|
7ee875ee70 | ||
|
|
9936b38e09 | ||
|
|
7e22674d40 | ||
|
|
ab644bd732 | ||
|
|
2becc4157a | ||
|
|
ec5fdecf91 | ||
|
|
bd88ba2277 | ||
|
|
a61857b37d | ||
|
|
866b0a215c | ||
|
|
2ba825db9b | ||
|
|
041665dee1 | ||
|
|
8072ded532 | ||
|
|
9956b5462d | ||
|
|
92d3b98448 | ||
|
|
46f34d64a0 | ||
|
|
d494de7551 | ||
|
|
129778945f | ||
|
|
f3183b267a | ||
|
|
6812aa0c5b | ||
|
|
89bf6742e3 | ||
|
|
f45d673d94 | ||
|
|
67400f048b | ||
|
|
d64d6f191f | ||
|
|
5345bfc43d | ||
|
|
e9745be0e8 | ||
|
|
bc8bf6941d | ||
|
|
f35444fabc | ||
|
|
2c1f810163 | ||
|
|
77570f03eb | ||
|
|
a8cfcead70 | ||
|
|
00f579c729 | ||
|
|
49b6792399 | ||
|
|
6b1982274e | ||
|
|
bc58640a3a | ||
|
|
511af7946e | ||
|
|
dcdb3d9d7a | ||
|
|
262dffd4bf | ||
|
|
2d5d2b21ee | ||
|
|
5c77cfc68d | ||
|
|
11f8e36ff0 | ||
|
|
44e4e66f5c | ||
|
|
3c4c6c3dfd | ||
|
|
bebebc46dc | ||
|
|
403d7f9c29 | ||
|
|
09ad397ee6 | ||
|
|
e56e1f8e21 | ||
|
|
00f232e2d7 | ||
|
|
a820f66bdc | ||
|
|
82d318a1c0 | ||
|
|
97afbf781e | ||
|
|
43a5f8858b | ||
|
|
dc04cd8781 | ||
|
|
793b6e92d8 | ||
|
|
c0b9939580 | ||
|
|
e248831719 | ||
|
|
25f1f6e641 | ||
|
|
d2f9762fbf | ||
|
|
7a89cfc02b | ||
|
|
64e22ba27d | ||
|
|
8c58916bb1 | ||
|
|
18cdd51680 | ||
|
|
db7b7639a0 | ||
|
|
1ad26b0c39 | ||
|
|
e1abecb556 | ||
|
|
6e642f45d8 | ||
|
|
9f9bfc9df0 | ||
|
|
866b92910f | ||
|
|
e071f6ec8d | ||
|
|
3eff58fcf9 | ||
|
|
540358d0f9 | ||
|
|
0e3be98d24 | ||
|
|
c39e075766 | ||
|
|
54935dd6f6 | ||
|
|
093660ab1a | ||
|
|
fb65839032 | ||
|
|
461a5942fb | ||
|
|
eee2e1be88 | ||
|
|
2d43955abf | ||
|
|
c5bed6e8c4 | ||
|
|
2320a1432a | ||
|
|
5d74cd6f89 | ||
|
|
273408e9ed | ||
|
|
5ccddf6d21 | ||
|
|
d97890fc4e | ||
|
|
4c1a9b1dd7 | ||
|
|
2168fb0d5b | ||
|
|
3bc54a6c2e | ||
|
|
444fcbb2b3 | ||
|
|
881e3e5953 | ||
|
|
5889f4c74b | ||
|
|
ee93b16558 | ||
|
|
e94c9db2ec | ||
|
|
9f88e7a003 | ||
|
|
b9bedfda21 | ||
|
|
d1529afeb3 | ||
|
|
99dece953f | ||
|
|
9ef000e529 | ||
|
|
db4ee4c01e | ||
|
|
0decdf5aab | ||
|
|
e460290056 | ||
|
|
b9130fd968 | ||
|
|
37980f3580 | ||
|
|
56bd3db7a2 | ||
|
|
18123d7f7e | ||
|
|
a04a99373d | ||
|
|
e906315969 | ||
|
|
a360a5fe3a | ||
|
|
fcbdc131ec | ||
|
|
857a5d8a91 | ||
|
|
e914481325 | ||
|
|
c0ba8b2ebb | ||
|
|
69507d1b00 | ||
|
|
311e357451 | ||
|
|
d1cd95c492 | ||
|
|
0f55f0e9a6 | ||
|
|
c26d78cf05 | ||
|
|
c3662443e4 | ||
|
|
18c96303a6 | ||
|
|
5c236a65f7 | ||
|
|
1f29f7fdf1 | ||
|
|
cbae2465d2 | ||
|
|
4a4e32846b | ||
|
|
426fde40b1 | ||
|
|
e9a3bcf3e9 | ||
|
|
5b77f5eeba | ||
|
|
5417881a72 | ||
|
|
1cadaf1ed0 | ||
|
|
929e0a9a01 | ||
|
|
15aadbb101 | ||
|
|
7f021acb2e | ||
|
|
68faad4098 | ||
|
|
ff989ba524 | ||
|
|
fa60457a0f | ||
|
|
43737a9730 | ||
|
|
e6fb708c1c | ||
|
|
8ff8e6458e | ||
|
|
7d24433a42 | ||
|
|
7ae23e2340 | ||
|
|
940f625ec1 | ||
|
|
2ec9aa2ca9 | ||
|
|
c6dfd502d2 | ||
|
|
0aa8c91577 | ||
|
|
c09f53449a | ||
|
|
f34a66e9c6 | ||
|
|
04fed4140c | ||
|
|
76e183b5f7 | ||
|
|
911fb6e934 | ||
|
|
e8f98825e0 | ||
|
|
5619f6f451 | ||
|
|
8223d06d98 | ||
|
|
b47884a504 | ||
|
|
afa2af3014 | ||
|
|
55521b87b9 | ||
|
|
45978ab193 | ||
|
|
575ac4a36d | ||
|
|
db07ac0abb | ||
|
|
a754497d65 | ||
|
|
d669419d18 | ||
|
|
7d3c53dbef | ||
|
|
04a1513e73 | ||
|
|
03d2c0e306 | ||
|
|
853707dfb2 | ||
|
|
540dd97dbf | ||
|
|
9f372828b4 | ||
|
|
6a929b6e25 | ||
|
|
45f278ab15 | ||
|
|
a58fa21234 | ||
|
|
c8d6e07179 | ||
|
|
fdfc3f2ec3 | ||
|
|
3a321eb195 | ||
|
|
d21eb3b946 | ||
|
|
439b5404ae | ||
|
|
821f60897f | ||
|
|
5ff935723f | ||
|
|
51126fd7ae | ||
|
|
fec6a159e6 | ||
|
|
eee9a07f54 | ||
|
|
563fc433d5 | ||
|
|
53a54fde7c | ||
|
|
b0f2202b54 | ||
|
|
cf6c3bb76b | ||
|
|
772640e098 | ||
|
|
52d4ced37c | ||
|
|
3ed44f72ad | ||
|
|
7569f7eb7c | ||
|
|
6d28e289c4 | ||
|
|
22154f2337 | ||
|
|
df4dd0f5a4 | ||
|
|
5314d3e1f8 | ||
|
|
bb16975400 | ||
|
|
0ce624e6f6 | ||
|
|
50dc322230 | ||
|
|
fb36435231 | ||
|
|
e688783931 | ||
|
|
2f3dbeedcd | ||
|
|
cebb1cd6d4 | ||
|
|
020ec50258 | ||
|
|
df42cc9047 | ||
|
|
8a9b4e2df7 | ||
|
|
b9150aab79 | ||
|
|
d4efed0d3a | ||
|
|
db1ad9aac8 | ||
|
|
270c261203 | ||
|
|
3f974b1adf | ||
|
|
9eac88344d | ||
|
|
5d1f00bd19 | ||
|
|
8d3f3ba81d | ||
|
|
248299a725 | ||
|
|
e916283522 | ||
|
|
b7a32656d5 | ||
|
|
78eb5d9d64 | ||
|
|
14c7170159 | ||
|
|
415fceb11c | ||
|
|
05c8957c90 | ||
|
|
e4bacb169e | ||
|
|
608dd843d6 | ||
|
|
70b9fd8e03 | ||
|
|
21ad753fcf | ||
|
|
88aa7154db | ||
|
|
e0827fe4ad | ||
|
|
b1d63c9a5b | ||
|
|
3ea00e783c | ||
|
|
9361570895 | ||
|
|
50ded59c3c | ||
|
|
050fda1bdb | ||
|
|
713d487808 | ||
|
|
354e422be8 | ||
|
|
ff7d7c5256 | ||
|
|
b60b50e99a | ||
|
|
1e4af12e3d | ||
|
|
e0c8178932 | ||
|
|
93ebbf8b1f | ||
|
|
22a850ab14 | ||
|
|
aab501aa17 | ||
|
|
94d5b4a4d5 | ||
|
|
db37c9c6a7 | ||
|
|
a49cb55821 | ||
|
|
ac15413af8 | ||
|
|
141ee65c91 | ||
|
|
55d1fee82d | ||
|
|
33f94b3ef9 | ||
|
|
30448a65c5 | ||
|
|
9c33ee123c | ||
|
|
d88710501f | ||
|
|
ba0208910f | ||
|
|
5d9708901d | ||
|
|
96b36c6614 | ||
|
|
13de3e69ef | ||
|
|
215de33c35 | ||
|
|
790ac22f8d | ||
|
|
4d8d1e33d0 | ||
|
|
595bd82234 | ||
|
|
9515d10cef | ||
|
|
56481bbbe0 | ||
|
|
79e4854973 | ||
|
|
b4e7629e8b | ||
|
|
a818d10d40 | ||
|
|
bd4b0d2756 | ||
|
|
80e29b2aa4 | ||
|
|
54ebfd6aad | ||
|
|
606e2d5278 | ||
|
|
194b96f5c1 | ||
|
|
91dcd8d83b | ||
|
|
0396dee74d | ||
|
|
3045c5108a | ||
|
|
9bb12eb781 | ||
|
|
d64fe12751 | ||
|
|
f5503ae717 | ||
|
|
4c2a727cd7 | ||
|
|
0605fca115 | ||
|
|
a60311b002 | ||
|
|
bd2ee45664 | ||
|
|
89f7060a80 | ||
|
|
15d0337b34 | ||
|
|
8d4caa2a1a | ||
|
|
5aef794f97 | ||
|
|
d01d8d5bbc | ||
|
|
aaced362a0 | ||
|
|
178801d3a7 | ||
|
|
6931dae8fd | ||
|
|
036aab07e1 | ||
|
|
e61017a86f | ||
|
|
39065f9dfa | ||
|
|
6069673cdd | ||
|
|
e3ad6558a5 | ||
|
|
74c10c45a0 | ||
|
|
35fb862fa9 | ||
|
|
8f9b1af729 | ||
|
|
1e3a0dddf6 | ||
|
|
e8c4a3636a | ||
|
|
81eaa6426a | ||
|
|
0a946e4200 | ||
|
|
0a9137fbf9 | ||
|
|
8a1d914ed9 | ||
|
|
cf89e01a51 | ||
|
|
3b10a59572 | ||
|
|
4a904a0106 | ||
|
|
24d7f52d46 | ||
|
|
8ab3382f47 | ||
|
|
2bd54202ce | ||
|
|
2e8dd3840b | ||
|
|
b1f49d3c8d | ||
|
|
8e232ac5ef | ||
|
|
ac2f0bbbf8 | ||
|
|
cd1f5ca97b | ||
|
|
44614a75c2 | ||
|
|
8c028e75a0 | ||
|
|
3a7128ad4f | ||
|
|
869ef8ec87 | ||
|
|
683f24b24f | ||
|
|
332a3d41e3 | ||
|
|
e9714cfb32 | ||
|
|
0408899998 | ||
|
|
67bd5c8f6a | ||
|
|
662af595cb | ||
|
|
555b1d817e | ||
|
|
d289a1cba5 | ||
|
|
3b16bf7538 | ||
|
|
55262eeb46 | ||
|
|
5d8d14640c | ||
|
|
99db2ca46d | ||
|
|
9b382f58b8 | ||
|
|
b1a735d83f | ||
|
|
15e963cee1 | ||
|
|
1921c3dc80 | ||
|
|
757b8a0eef | ||
|
|
2e4c9fe027 | ||
|
|
35e71559a4 | ||
|
|
6bfd76ffc7 | ||
|
|
e536b88f1c | ||
|
|
ff1df11384 | ||
|
|
dcefca6038 | ||
|
|
6aefc71449 | ||
|
|
394afdba6d | ||
|
|
c698956f9f | ||
|
|
1ff9c9f39b | ||
|
|
b93f9c10d3 | ||
|
|
5962496d83 | ||
|
|
952ccf37fa | ||
|
|
4740aebd7f | ||
|
|
2a9fe7dbe2 | ||
|
|
44753b681d | ||
|
|
865ca54e86 | ||
|
|
71654ff9ff | ||
|
|
95ce66a66a | ||
|
|
4ed000609d | ||
|
|
024342ce12 | ||
|
|
24460967b3 | ||
|
|
94583c2684 | ||
|
|
7f7cb80e37 | ||
|
|
263a2ba547 | ||
|
|
3014651ac2 | ||
|
|
a7c9846245 | ||
|
|
d41d9095a1 | ||
|
|
ddf0c480a0 | ||
|
|
7091546b2b | ||
|
|
a7bcf063c3 | ||
|
|
f2e4637f29 | ||
|
|
021e1fd8c8 | ||
|
|
e46604707f | ||
|
|
eee8dcbbd6 | ||
|
|
025929035e | ||
|
|
1d15e12b8d | ||
|
|
552f4260b4 | ||
|
|
0c3b5fc3c5 | ||
|
|
42f59e99df | ||
|
|
5d84f26b72 | ||
|
|
60747082b6 | ||
|
|
1e00be4cf3 | ||
|
|
153469664c | ||
|
|
d40e9f1623 | ||
|
|
4f8bf413bd | ||
|
|
0558cc61cf | ||
|
|
a3761beae2 | ||
|
|
49aaaa8f26 | ||
|
|
a74da31964 | ||
|
|
e41eca0d8a | ||
|
|
48f4d01c70 | ||
|
|
6981669a68 | ||
|
|
ebd1ca44f6 | ||
|
|
12f0f4319b | ||
|
|
4c15f5efda | ||
|
|
ce61f49f27 | ||
|
|
313db1def9 | ||
|
|
57f6a69e94 | ||
|
|
ce085e01ec | ||
|
|
89f9a1179e | ||
|
|
eb5d55478d | ||
|
|
0855391f77 | ||
|
|
619fbfc6ea | ||
|
|
b644c947e3 | ||
|
|
1c03d837cc | ||
|
|
2e7aabd973 | ||
|
|
6147df5c6c | ||
|
|
54dcc58ee8 | ||
|
|
ff94c735e8 | ||
|
|
aa3f75390d | ||
|
|
f4a1ad6e61 | ||
|
|
87428b299f | ||
|
|
5b78f161a4 | ||
|
|
9688d71e23 | ||
|
|
02875ef7ab | ||
|
|
e42276affa | ||
|
|
f47aa13dd3 | ||
|
|
dcdf4f77db | ||
|
|
96a27d083c | ||
|
|
f995f451a7 | ||
|
|
a3838fc00f | ||
|
|
4db8131397 | ||
|
|
207bce6216 | ||
|
|
8fb8b80a12 | ||
|
|
b762689225 | ||
|
|
7bbb6e0b42 | ||
|
|
b587804e3e | ||
|
|
5660247da2 | ||
|
|
fc2c44a6b7 | ||
|
|
4eb8a6eb0f | ||
|
|
c8eaa11f80 | ||
|
|
55a9d76b9b | ||
|
|
7abd992d17 | ||
|
|
daeccaed36 | ||
|
|
25358a4077 | ||
|
|
7cf4f84e89 | ||
|
|
b63199e4e1 | ||
|
|
4af08be7e8 | ||
|
|
606148e19c | ||
|
|
d38e560265 | ||
|
|
ffd1bc07cc | ||
|
|
dfcfe5a370 | ||
|
|
803823eac0 | ||
|
|
5a18e09730 | ||
|
|
dd41f8c006 | ||
|
|
7a2a4817fe | ||
|
|
105522e5ab | ||
|
|
a52e201f86 | ||
|
|
697ff54125 | ||
|
|
4bcd34bd78 | ||
|
|
64c52c7a3c | ||
|
|
1c143eb9ea | ||
|
|
1da4ccfb8c | ||
|
|
a61b22b821 | ||
|
|
d07e0edd88 | ||
|
|
c2821ab77b | ||
|
|
1696db930d | ||
|
|
9397b3fd5a | ||
|
|
2bb11db412 | ||
|
|
c2f3e00d99 | ||
|
|
292fe66d3f | ||
|
|
ba46015bae | ||
|
|
12d5915c8e | ||
|
|
c6fe45371c | ||
|
|
2fdb4dd0bd | ||
|
|
968e70b69b | ||
|
|
6a7c47f3a9 | ||
|
|
7daf295e63 | ||
|
|
7dddc0a85f | ||
|
|
32523d474d | ||
|
|
841acf0203 | ||
|
|
b4ab1a5375 | ||
|
|
3ec9853b75 | ||
|
|
c1d140a831 | ||
|
|
02dc4d6a30 | ||
|
|
0d366a350d | ||
|
|
1e8b0c36a1 | ||
|
|
0af592a5f4 | ||
|
|
86b2a5c772 | ||
|
|
9cfd50dc4f | ||
|
|
698b19c8fa | ||
|
|
5d4bc99097 | ||
|
|
cfb3f1ca42 | ||
|
|
01b02ccd84 | ||
|
|
ee1605a5e6 | ||
|
|
f3dca74e87 | ||
|
|
d57b79427b | ||
|
|
8a89c0c53f | ||
|
|
a8a869dd49 | ||
|
|
c260e188b0 | ||
|
|
230b9c17eb | ||
|
|
2a8b5cf911 | ||
|
|
3fa88a3e56 | ||
|
|
64532c3d08 | ||
|
|
5f86ab659e | ||
|
|
df14a94918 | ||
|
|
e087e48088 | ||
|
|
5110b37a82 | ||
|
|
a2bb231424 | ||
|
|
e939f742a8 | ||
|
|
9093be748e | ||
|
|
e23a7e67a0 | ||
|
|
273b4fb7a7 | ||
|
|
0b2684d532 | ||
|
|
224192a1ee | ||
|
|
2d7202ac81 | ||
|
|
8d95428fa6 | ||
|
|
d678376d8d | ||
|
|
9b5fa83da3 | ||
|
|
0f1c3e1ace | ||
|
|
f3cb610748 | ||
|
|
1e6316d414 | ||
|
|
df51cbe21b | ||
|
|
dce31b212b | ||
|
|
00269d3dfb | ||
|
|
648be3969a | ||
|
|
a54325fdcf | ||
|
|
b64e5929b4 | ||
|
|
631cbcd352 | ||
|
|
24c5a5bd13 | ||
|
|
1103947af0 | ||
|
|
061de3c12f | ||
|
|
207887a28d | ||
|
|
dc53f973e4 | ||
|
|
9a80a488cd | ||
|
|
a85b6c798a | ||
|
|
9229809b05 | ||
|
|
f81cf6b600 | ||
|
|
ba4ffc7272 | ||
|
|
12695474e3 | ||
|
|
9d1dd5829d | ||
|
|
d4148891fc | ||
|
|
c9c16f54b1 | ||
|
|
7da72fe9cb | ||
|
|
515c6e13f0 | ||
|
|
5abdc063eb | ||
|
|
360e1adc22 | ||
|
|
8c6ac53a05 | ||
|
|
b636edb32f | ||
|
|
d02c6d8ce9 | ||
|
|
88f6557aaf | ||
|
|
b476024612 | ||
|
|
a3808c31ef | ||
|
|
5d251f6f9a | ||
|
|
117332d0b4 | ||
|
|
2bbc0c00cc | ||
|
|
5e4ad9b338 | ||
|
|
a9a422c892 | ||
|
|
5b6bbacab0 | ||
|
|
35677c553c | ||
|
|
df357d2327 | ||
|
|
e6ff7ee4fc | ||
|
|
30eea4b8f7 | ||
|
|
7ad047a432 | ||
|
|
e291a260b2 | ||
|
|
9a9e66b43d | ||
|
|
1c3deb648a | ||
|
|
b39311de85 | ||
|
|
5417a0cdcf | ||
|
|
6a4ea43d27 | ||
|
|
7fc1ade46c | ||
|
|
635fc80ef2 | ||
|
|
c59d805717 | ||
|
|
3394614ecf | ||
|
|
06c8ab9c37 | ||
|
|
6a0d0a0422 | ||
|
|
9470771561 | ||
|
|
8132e1f5ea | ||
|
|
0b28bf0e96 | ||
|
|
13f023d364 | ||
|
|
eaefe4a908 | ||
|
|
f47b657c6f | ||
|
|
800a2efa50 | ||
|
|
7465e8e432 | ||
|
|
86ab3804e5 | ||
|
|
b749d47a61 | ||
|
|
032c7fd16b | ||
|
|
b9579ab4b4 | ||
|
|
c7632b57a6 | ||
|
|
da17e66961 | ||
|
|
cd0157347f | ||
|
|
76a8362e3d | ||
|
|
c8cc2edd36 | ||
|
|
5f8016c67e | ||
|
|
3d88d6a2c7 | ||
|
|
9c3a9e233b | ||
|
|
c43509c28e | ||
|
|
0e0bfc82b5 | ||
|
|
2f660db511 | ||
|
|
fdc5a30060 | ||
|
|
bb1d96ed5d | ||
|
|
59501905ab | ||
|
|
cb870422c3 | ||
|
|
2b85732e36 | ||
|
|
284c749bb5 | ||
|
|
d7f5e73f84 | ||
|
|
cde69da173 | ||
|
|
bcc75f6b00 | ||
|
|
f179381b64 | ||
|
|
10ba53b897 | ||
|
|
1cfe3b1bb2 | ||
|
|
268ff9c826 | ||
|
|
7eb184d946 | ||
|
|
1cc34a1485 | ||
|
|
aa2f2f3627 | ||
|
|
7e373e2f9c | ||
|
|
942e6f263c | ||
|
|
2e6d7d28ba | ||
|
|
3a823ea457 | ||
|
|
4daba6cfb4 | ||
|
|
fa018e2e0c | ||
|
|
315d25f7c0 | ||
|
|
cc7ffada96 | ||
|
|
5394c1bc91 | ||
|
|
b230b73829 | ||
|
|
930a68ab4a | ||
|
|
4e833981e4 | ||
|
|
2ea402ff10 | ||
|
|
da6b024edc | ||
|
|
565e9a3cea | ||
|
|
3c10d19a7e | ||
|
|
cf96e047cd | ||
|
|
6f6a8e1272 | ||
|
|
c2a02e7074 | ||
|
|
7d2cf97fbf | ||
|
|
195469897c | ||
|
|
c7b422d465 | ||
|
|
2dbace37bb | ||
|
|
2e903fd42c | ||
|
|
47a104b29c | ||
|
|
d248581523 | ||
|
|
48f6300e08 | ||
|
|
f7a143cb78 | ||
|
|
2f7cd97852 | ||
|
|
73eca75ed9 | ||
|
|
2ade2989e8 | ||
|
|
b1d5acf2ad | ||
|
|
5d5ec6b9be | ||
|
|
1b84023c5d | ||
|
|
97f3329c1b | ||
|
|
bdeb344999 | ||
|
|
52e1de954f | ||
|
|
7b9eafef4e | ||
|
|
8b141a0c28 | ||
|
|
7f602d7fcc |
@@ -40,6 +40,7 @@ develop-eggs
|
|||||||
coverage.xml
|
coverage.xml
|
||||||
htmlcov
|
htmlcov
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
|
.python-version
|
||||||
|
|
||||||
# C Extensions
|
# C Extensions
|
||||||
*.o
|
*.o
|
||||||
|
|||||||
+5
-5
@@ -1,23 +1,23 @@
|
|||||||
#
|
#
|
||||||
# Dockerfile for an image with the currently checked out version of catalyst installed. To build:
|
# Dockerfile for an image with the currently checked out version of catalyst installed. To build:
|
||||||
#
|
#
|
||||||
# docker build -t quantopian/catalyst .
|
# docker build -t enigmampc/catalyst .
|
||||||
#
|
#
|
||||||
# To run the container:
|
# To run the container:
|
||||||
#
|
#
|
||||||
# docker run -v /path/to/your/notebooks:/projects -v ~/.catalyst:/root/.catalyst -p 8888:8888/tcp --name catalyst -it quantopian/catalyst
|
# docker run -v /path/to/your/notebooks:/projects -v ~/.catalyst:/root/.catalyst -p 8888:8888/tcp --name catalyst -it enigmampc/catalyst
|
||||||
#
|
#
|
||||||
# To access Jupyter when running docker locally (you may need to add NAT rules):
|
# To access Jupyter when running docker locally (you may need to add NAT rules):
|
||||||
#
|
#
|
||||||
# https://127.0.0.1
|
# https://127.0.0.1
|
||||||
#
|
#
|
||||||
# default password is jupyter. to provide another, see:
|
# Default password is 'jupyter'. To provide another, see:
|
||||||
# http://jupyter-notebook.readthedocs.org/en/latest/public_server.html#preparing-a-hashed-password
|
# http://jupyter-notebook.readthedocs.org/en/latest/public_server.html#preparing-a-hashed-password
|
||||||
#
|
#
|
||||||
# once generated, you can pass the new value via `docker run --env` the first time
|
# Once generated, you can pass the new value via `docker run --env` the first time
|
||||||
# you start the container.
|
# you start the container.
|
||||||
#
|
#
|
||||||
# You can also run an algo using the docker exec command. For example:
|
# You can also run an algo using the docker exec command. For example:
|
||||||
#
|
#
|
||||||
# docker exec -it catalyst catalyst run -f /projects/my_algo.py --start 2015-1-1 --end 2016-1-1 /projects/result.pickle
|
# docker exec -it catalyst catalyst run -f /projects/my_algo.py --start 2015-1-1 --end 2016-1-1 /projects/result.pickle
|
||||||
#
|
#
|
||||||
|
|||||||
+8
-8
@@ -1,31 +1,31 @@
|
|||||||
#
|
#
|
||||||
# Dockerfile for an image with the currently checked out version of catalyst installed. To build:
|
# Dockerfile for an image with the currently checked out version of catalyst installed. To build:
|
||||||
#
|
#
|
||||||
# docker build -t quantopian/catalystdev -f Dockerfile-dev .
|
# docker build -t enigmampc/catalystdev -f Dockerfile-dev .
|
||||||
#
|
#
|
||||||
# Note: the dev build requires a quantopian/catalyst image, which you can build as follows:
|
# Note: the dev build requires a enigmampc/catalyst image, which you can build as follows:
|
||||||
#
|
#
|
||||||
# docker build -t quantopian/catalyst -f Dockerfile
|
# docker build -t enigmampc/catalyst -f Dockerfile .
|
||||||
#
|
#
|
||||||
# To run the container:
|
# To run the container:
|
||||||
#
|
#
|
||||||
# docker run -v /path/to/your/notebooks:/projects -v ~/.catalyst:/root/.catalyst -p 8888:8888/tcp --name catalystdev -it quantopian/catalystdev
|
# docker run -v /path/to/your/notebooks:/projects -v ~/.catalyst:/root/.catalyst -p 8888:8888/tcp --name catalystdev -it enigmampc/catalystdev
|
||||||
#
|
#
|
||||||
# To access Jupyter when running docker locally (you may need to add NAT rules):
|
# To access Jupyter when running docker locally (you may need to add NAT rules):
|
||||||
#
|
#
|
||||||
# https://127.0.0.1
|
# https://127.0.0.1
|
||||||
#
|
#
|
||||||
# default password is jupyter. to provide another, see:
|
# Default password is 'jupyter'. To provide another, see:
|
||||||
# http://jupyter-notebook.readthedocs.org/en/latest/public_server.html#preparing-a-hashed-password
|
# http://jupyter-notebook.readthedocs.org/en/latest/public_server.html#preparing-a-hashed-password
|
||||||
#
|
#
|
||||||
# once generated, you can pass the new value via `docker run --env` the first time
|
# Once generated, you can pass the new value via `docker run --env` the first time
|
||||||
# you start the container.
|
# you start the container.
|
||||||
#
|
#
|
||||||
# You can also run an algo using the docker exec command. For example:
|
# You can also run an algo using the docker exec command. For example:
|
||||||
#
|
#
|
||||||
# docker exec -it catalystdev catalyst run -f /projects/my_algo.py --start 2015-1-1 --end 2016-1-1 /projects/result.pickle
|
# docker exec -it catalystdev catalyst run -f /projects/my_algo.py --start 2015-1-1 --end 2016-1-1 /projects/result.pickle
|
||||||
#
|
#
|
||||||
FROM quantopian/catalyst
|
FROM enigmampc/catalyst
|
||||||
|
|
||||||
WORKDIR /catalyst
|
WORKDIR /catalyst
|
||||||
|
|
||||||
|
|||||||
+76
-1
@@ -1 +1,76 @@
|
|||||||
All the documentation for `Catalyst <https://github.com/enigmampc/catalyst>`_ can be found in the `catalyst-docs wiki <https://github.com/enigmampc/catalyst-docs/wiki>`_.
|
.. image:: https://s3.amazonaws.com/enigmaco-docs/enigma-catalyst.png
|
||||||
|
:target: https://enigmampc.github.io/catalyst
|
||||||
|
:align: center
|
||||||
|
:alt: Enigma | Catalyst
|
||||||
|
|
||||||
|
|version tag|
|
||||||
|
|version status|
|
||||||
|
|forum|
|
||||||
|
|discord|
|
||||||
|
|twitter|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
Catalyst is an algorithmic trading library for crypto-assets written in Python.
|
||||||
|
It allows trading strategies to be easily expressed and backtested against
|
||||||
|
historical data (with daily and minute resolution), providing analytics and
|
||||||
|
insights regarding a particular strategy's performance. Catalyst also supports
|
||||||
|
live-trading of crypto-assets starting with three exchanges (Bitfinex, Bittrex,
|
||||||
|
and Poloniex) with more being added over time. Catalyst empowers users to share
|
||||||
|
and curate data and build profitable, data-driven investment strategies. Please
|
||||||
|
visit `enigma.co <https://www.enigma.co>`_ to learn more about Catalyst.
|
||||||
|
|
||||||
|
Catalyst builds on top of the well-established
|
||||||
|
`Zipline <https://github.com/quantopian/zipline>`_ project. We did our best to
|
||||||
|
minimize structural changes to the general API to maximize compatibility with
|
||||||
|
existing trading algorithms, developer knowledge, and tutorials. Join us on the
|
||||||
|
`Catalyst Forum <https://catalyst.enigma.co/>`_ for questions around Catalyst,
|
||||||
|
algorithmic trading and technical support. We also have a
|
||||||
|
`Discord <https://discord.gg/SJK32GY>`_ group with the *#catalyst_dev* and
|
||||||
|
*#catalyst_setup* dedicated channels.
|
||||||
|
|
||||||
|
Overview
|
||||||
|
========
|
||||||
|
|
||||||
|
- Ease of use: Catalyst tries to get out of your way so that you can
|
||||||
|
focus on algorithm development. See
|
||||||
|
`examples of trading strategies <https://github.com/enigmampc/catalyst/tree/master/catalyst/examples>`_
|
||||||
|
provided.
|
||||||
|
- Support for several of the top crypto-exchanges by trading volume:
|
||||||
|
`Bitfinex <https://www.bitfinex.com>`_, `Bittrex <http://www.bittrex.com>`_,
|
||||||
|
and `Poloniex <https://www.poloniex.com>`_.
|
||||||
|
- Secure: You and only you have access to each exchange API keys for your accounts.
|
||||||
|
- Input of historical pricing data of all crypto-assets by exchange,
|
||||||
|
with daily and minute resolution. See
|
||||||
|
`Catalyst Market Coverage Overview <https://www.enigma.co/catalyst/status>`_.
|
||||||
|
- Backtesting and live-trading functionality, with a seamless transition
|
||||||
|
between the two modes.
|
||||||
|
- Output of performance statistics are based on Pandas DataFrames to
|
||||||
|
integrate nicely into the existing PyData eco-system.
|
||||||
|
- Statistic and machine learning libraries like matplotlib, scipy,
|
||||||
|
statsmodels, and sklearn support development, analysis, and
|
||||||
|
visualization of state-of-the-art trading systems.
|
||||||
|
- Addition of Bitcoin price (btc_usdt) as a benchmark for comparing
|
||||||
|
performance across trading algorithms.
|
||||||
|
|
||||||
|
Go to our `Documentation Website <https://enigmampc.github.io/catalyst/>`_.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. |version tag| image:: https://img.shields.io/pypi/v/enigma-catalyst.svg
|
||||||
|
:target: https://pypi.python.org/pypi/enigma-catalyst
|
||||||
|
|
||||||
|
.. |version status| image:: https://img.shields.io/pypi/pyversions/enigma-catalyst.svg
|
||||||
|
:target: https://pypi.python.org/pypi/enigma-catalyst
|
||||||
|
|
||||||
|
.. |forum| image:: https://img.shields.io/badge/forum-join-green.svg
|
||||||
|
:target: https://catalyst.enigma.co/
|
||||||
|
|
||||||
|
.. |discord| image:: https://img.shields.io/badge/discord-join%20chat-green.svg
|
||||||
|
:target: https://discordapp.com/invite/SJK32GY
|
||||||
|
|
||||||
|
.. |twitter| image:: https://img.shields.io/twitter/follow/enigmampc.svg?style=social&label=Follow&style=flat-square
|
||||||
|
:target: https://twitter.com/enigmampc
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+4
-10
@@ -29,11 +29,14 @@ from ._version import get_versions
|
|||||||
from . algorithm import TradingAlgorithm
|
from . algorithm import TradingAlgorithm
|
||||||
from . import api
|
from . import api
|
||||||
|
|
||||||
|
from catalyst.utils.calendars.calendar_utils import global_calendar_dispatcher
|
||||||
|
|
||||||
|
__version__ = get_versions()['version']
|
||||||
|
del get_versions
|
||||||
|
|
||||||
# PERF: Fire a warning if calendars were instantiated during catalyst import.
|
# PERF: Fire a warning if calendars were instantiated during catalyst import.
|
||||||
# Having calendars doesn't break anything per-se, but it makes catalyst imports
|
# Having calendars doesn't break anything per-se, but it makes catalyst imports
|
||||||
# noticeably slower, which becomes particularly noticeable in the Zipline CLI.
|
# noticeably slower, which becomes particularly noticeable in the Zipline CLI.
|
||||||
from catalyst.utils.calendars.calendar_utils import global_calendar_dispatcher
|
|
||||||
if global_calendar_dispatcher._calendars:
|
if global_calendar_dispatcher._calendars:
|
||||||
import warnings
|
import warnings
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
@@ -44,10 +47,6 @@ if global_calendar_dispatcher._calendars:
|
|||||||
del global_calendar_dispatcher
|
del global_calendar_dispatcher
|
||||||
|
|
||||||
|
|
||||||
__version__ = get_versions()['version']
|
|
||||||
del get_versions
|
|
||||||
|
|
||||||
|
|
||||||
def load_ipython_extension(ipython):
|
def load_ipython_extension(ipython):
|
||||||
from .__main__ import catalyst_magic
|
from .__main__ import catalyst_magic
|
||||||
ipython.register_magic_function(catalyst_magic, 'line_cell', 'catalyst')
|
ipython.register_magic_function(catalyst_magic, 'line_cell', 'catalyst')
|
||||||
@@ -69,7 +68,6 @@ if os.name == 'nt':
|
|||||||
_()
|
_()
|
||||||
del _
|
del _
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'TradingAlgorithm',
|
'TradingAlgorithm',
|
||||||
'api',
|
'api',
|
||||||
@@ -80,7 +78,3 @@ __all__ = [
|
|||||||
'run_algorithm',
|
'run_algorithm',
|
||||||
'utils',
|
'utils',
|
||||||
]
|
]
|
||||||
|
|
||||||
from ._version import get_versions
|
|
||||||
__version__ = get_versions()['version']
|
|
||||||
del get_versions
|
|
||||||
|
|||||||
+300
-43
@@ -3,13 +3,15 @@ import os
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import sys
|
||||||
import logbook
|
import logbook
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.marketplace.marketplace import Marketplace
|
||||||
from six import text_type
|
from six import text_type
|
||||||
|
|
||||||
from catalyst.data import bundles as bundles_module
|
from catalyst.data import bundles as bundles_module
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
from catalyst.exchange.init_utils import get_exchange
|
from catalyst.exchange.utils.exchange_utils import delete_algo_folder
|
||||||
from catalyst.utils.cli import Date, Timestamp
|
from catalyst.utils.cli import Date, Timestamp
|
||||||
from catalyst.utils.run_algo import _run, load_extensions
|
from catalyst.utils.run_algo import _run, load_extensions
|
||||||
|
|
||||||
@@ -29,16 +31,17 @@ except NameError:
|
|||||||
@click.option(
|
@click.option(
|
||||||
'--strict-extensions/--non-strict-extensions',
|
'--strict-extensions/--non-strict-extensions',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help='If --strict-extensions is passed then catalyst will not run if it'
|
help='If --strict-extensions is passed then catalyst will not run '
|
||||||
' cannot load all of the specified extensions. If this is not passed or'
|
'if it cannot load all of the specified extensions. If this is '
|
||||||
' --non-strict-extensions is passed then the failure will be logged but'
|
'not passed or --non-strict-extensions is passed then the '
|
||||||
' execution will continue.',
|
'failure will be logged but execution will continue.',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--default-extension/--no-default-extension',
|
'--default-extension/--no-default-extension',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=True,
|
default=True,
|
||||||
help="Don't load the default catalyst extension.py file in $ZIPLINE_HOME.",
|
help="Don't load the default catalyst extension.py file "
|
||||||
|
"in $CATALYST_HOME.",
|
||||||
)
|
)
|
||||||
@click.version_option()
|
@click.version_option()
|
||||||
def main(extension, strict_extensions, default_extension):
|
def main(extension, strict_extensions, default_extension):
|
||||||
@@ -123,9 +126,9 @@ def ipython_only(option):
|
|||||||
'--define',
|
'--define',
|
||||||
multiple=True,
|
multiple=True,
|
||||||
help="Define a name to be bound in the namespace before executing"
|
help="Define a name to be bound in the namespace before executing"
|
||||||
" the algotext. For example '-Dname=value'. The value may be any python"
|
" the algotext. For example '-Dname=value'. The value may be"
|
||||||
" expression. These are evaluated in order so they may refer to previously"
|
" any python expression. These are evaluated in order so they"
|
||||||
" defined names.",
|
" may refer to previously defined names.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--data-frequency',
|
'--data-frequency',
|
||||||
@@ -137,7 +140,6 @@ def ipython_only(option):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'--capital-base',
|
'--capital-base',
|
||||||
type=float,
|
type=float,
|
||||||
default=10e6,
|
|
||||||
show_default=True,
|
show_default=True,
|
||||||
help='The starting capital for the simulation.',
|
help='The starting capital for the simulation.',
|
||||||
)
|
)
|
||||||
@@ -175,8 +177,8 @@ def ipython_only(option):
|
|||||||
default='-',
|
default='-',
|
||||||
metavar='FILENAME',
|
metavar='FILENAME',
|
||||||
show_default=True,
|
show_default=True,
|
||||||
help="The location to write the perf data. If this is '-' the perf will"
|
help="The location to write the perf data. If this is '-' the perf"
|
||||||
" be written to stdout.",
|
" will be written to stdout.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--print-algo/--no-print-algo',
|
'--print-algo/--no-print-algo',
|
||||||
@@ -193,8 +195,7 @@ def ipython_only(option):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
help='The name of the targeted exchange.',
|
||||||
help='The name of the targeted exchange (supported: bitfinex, bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-n',
|
'-n',
|
||||||
@@ -239,16 +240,27 @@ def run(ctx,
|
|||||||
# does not pass either of these and then passes the first only
|
# does not pass either of these and then passes the first only
|
||||||
# to be told they need to pass the second argument also
|
# to be told they need to pass the second argument also
|
||||||
ctx.fail(
|
ctx.fail(
|
||||||
"must specify dates with '-s' / '--start' and '-e' / '--end'",
|
"must specify dates with '-s' / '--start' and '-e' / '--end'"
|
||||||
|
" in backtest mode",
|
||||||
)
|
)
|
||||||
if start is None:
|
if start is None:
|
||||||
ctx.fail("must specify a start date with '-s' / '--start'")
|
ctx.fail("must specify a start date with '-s' / '--start'"
|
||||||
|
" in backtest mode")
|
||||||
if end is None:
|
if end is None:
|
||||||
ctx.fail("must specify an end date with '-e' / '--end'")
|
ctx.fail("must specify an end date with '-e' / '--end'"
|
||||||
|
" in backtest mode")
|
||||||
|
|
||||||
if exchange_name is None:
|
if exchange_name is None:
|
||||||
ctx.fail("must specify an exchange name '-x'")
|
ctx.fail("must specify an exchange name '-x'")
|
||||||
|
|
||||||
|
if base_currency is None:
|
||||||
|
ctx.fail("must specify a base currency with '-c' in backtest mode")
|
||||||
|
|
||||||
|
if capital_base is None:
|
||||||
|
ctx.fail("must specify a capital base with '--capital-base'")
|
||||||
|
|
||||||
|
click.echo('Running in backtesting mode.', sys.stdout)
|
||||||
|
|
||||||
perf = _run(
|
perf = _run(
|
||||||
initialize=None,
|
initialize=None,
|
||||||
handle_data=None,
|
handle_data=None,
|
||||||
@@ -272,11 +284,15 @@ def run(ctx,
|
|||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
algo_namespace=algo_namespace,
|
algo_namespace=algo_namespace,
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
live_graph=False
|
analyze_live=None,
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=True,
|
||||||
|
auth_aliases=None,
|
||||||
|
stats_output=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
click.echo(str(perf))
|
click.echo(str(perf), sys.stdout)
|
||||||
elif output != os.devnull: # make the catalyst magic not write any data
|
elif output != os.devnull: # make the catalyst magic not write any data
|
||||||
perf.to_pickle(output)
|
perf.to_pickle(output)
|
||||||
|
|
||||||
@@ -324,6 +340,12 @@ def catalyst_magic(line, cell=None):
|
|||||||
type=click.File('r'),
|
type=click.File('r'),
|
||||||
help='The file that contains the algorithm to run.',
|
help='The file that contains the algorithm to run.',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--capital-base',
|
||||||
|
type=float,
|
||||||
|
show_default=True,
|
||||||
|
help='The amount of capital (in base_currency) allocated to trading.',
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-t',
|
'-t',
|
||||||
'--algotext',
|
'--algotext',
|
||||||
@@ -334,9 +356,9 @@ def catalyst_magic(line, cell=None):
|
|||||||
'--define',
|
'--define',
|
||||||
multiple=True,
|
multiple=True,
|
||||||
help="Define a name to be bound in the namespace before executing"
|
help="Define a name to be bound in the namespace before executing"
|
||||||
" the algotext. For example '-Dname=value'. The value may be any python"
|
" the algotext. For example '-Dname=value'. The value may be"
|
||||||
" expression. These are evaluated in order so they may refer to previously"
|
" any python expression. These are evaluated in order so they"
|
||||||
" defined names.",
|
" may refer to previously defined names.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-o',
|
'-o',
|
||||||
@@ -362,8 +384,7 @@ def catalyst_magic(line, cell=None):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
help='The name of the targeted exchange.',
|
||||||
help='The name of the targeted exchange (supported: bitfinex, bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-n',
|
'-n',
|
||||||
@@ -376,15 +397,38 @@ def catalyst_magic(line, cell=None):
|
|||||||
help='The base currency used to calculate statistics '
|
help='The base currency used to calculate statistics '
|
||||||
'(e.g. usd, btc, eth).',
|
'(e.g. usd, btc, eth).',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'-e',
|
||||||
|
'--end',
|
||||||
|
type=Date(tz='utc', as_timestamp=True),
|
||||||
|
help='An optional end date at which to stop the execution.',
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--live-graph/--no-live-graph',
|
'--live-graph/--no-live-graph',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=False,
|
default=False,
|
||||||
help='Display live graph.',
|
help='Display live graph.',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--simulate-orders/--no-simulate-orders',
|
||||||
|
is_flag=True,
|
||||||
|
default=True,
|
||||||
|
help='Simulating orders enable the paper trading mode. No orders will be '
|
||||||
|
'sent to the exchange unless set to false.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--auth-aliases',
|
||||||
|
default=None,
|
||||||
|
help='Authentication file aliases for the specified exchanges. By default,'
|
||||||
|
'each exchange uses the "auth.json" file in the exchange folder. '
|
||||||
|
'Specifying an "auth2" alias would use "auth2.json". It should be '
|
||||||
|
'specified like this: "[exchange_name],[alias],..." For example, '
|
||||||
|
'"binance,auth2" or "binance,auth2,bittrex,auth2".',
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def live(ctx,
|
def live(ctx,
|
||||||
algofile,
|
algofile,
|
||||||
|
capital_base,
|
||||||
algotext,
|
algotext,
|
||||||
define,
|
define,
|
||||||
output,
|
output,
|
||||||
@@ -393,7 +437,10 @@ def live(ctx,
|
|||||||
exchange_name,
|
exchange_name,
|
||||||
algo_namespace,
|
algo_namespace,
|
||||||
base_currency,
|
base_currency,
|
||||||
live_graph):
|
end,
|
||||||
|
live_graph,
|
||||||
|
auth_aliases,
|
||||||
|
simulate_orders):
|
||||||
"""Trade live with the given algorithm.
|
"""Trade live with the given algorithm.
|
||||||
"""
|
"""
|
||||||
if (algotext is not None) == (algofile is not None):
|
if (algotext is not None) == (algofile is not None):
|
||||||
@@ -404,11 +451,22 @@ def live(ctx,
|
|||||||
|
|
||||||
if exchange_name is None:
|
if exchange_name is None:
|
||||||
ctx.fail("must specify an exchange name '-x'")
|
ctx.fail("must specify an exchange name '-x'")
|
||||||
|
|
||||||
if algo_namespace is None:
|
if algo_namespace is None:
|
||||||
ctx.fail("must specify an algorithm name '-n' in live execution mode")
|
ctx.fail("must specify an algorithm name '-n' in live execution mode")
|
||||||
|
|
||||||
if base_currency is None:
|
if base_currency is None:
|
||||||
ctx.fail("must specify a base currency '-c' in live execution mode")
|
ctx.fail("must specify a base currency '-c' in live execution mode")
|
||||||
|
|
||||||
|
if capital_base is None:
|
||||||
|
ctx.fail("must specify a capital base with '--capital-base'")
|
||||||
|
|
||||||
|
if simulate_orders:
|
||||||
|
click.echo('Running in paper trading mode.', sys.stdout)
|
||||||
|
|
||||||
|
else:
|
||||||
|
click.echo('Running in live trading mode.', sys.stdout)
|
||||||
|
|
||||||
perf = _run(
|
perf = _run(
|
||||||
initialize=None,
|
initialize=None,
|
||||||
handle_data=None,
|
handle_data=None,
|
||||||
@@ -418,12 +476,12 @@ def live(ctx,
|
|||||||
algotext=algotext,
|
algotext=algotext,
|
||||||
defines=define,
|
defines=define,
|
||||||
data_frequency=None,
|
data_frequency=None,
|
||||||
capital_base=None,
|
capital_base=capital_base,
|
||||||
data=None,
|
data=None,
|
||||||
bundle=None,
|
bundle=None,
|
||||||
bundle_timestamp=None,
|
bundle_timestamp=None,
|
||||||
start=None,
|
start=None,
|
||||||
end=None,
|
end=end,
|
||||||
output=output,
|
output=output,
|
||||||
print_algo=print_algo,
|
print_algo=print_algo,
|
||||||
local_namespace=local_namespace,
|
local_namespace=local_namespace,
|
||||||
@@ -432,11 +490,15 @@ def live(ctx,
|
|||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
algo_namespace=algo_namespace,
|
algo_namespace=algo_namespace,
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
live_graph=live_graph
|
live_graph=live_graph,
|
||||||
|
analyze_live=None,
|
||||||
|
simulate_orders=simulate_orders,
|
||||||
|
auth_aliases=auth_aliases,
|
||||||
|
stats_output=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
click.echo(str(perf))
|
click.echo(str(perf), sys.stdout)
|
||||||
elif output != os.devnull: # make the catalyst magic not write any data
|
elif output != os.devnull: # make the catalyst magic not write any data
|
||||||
perf.to_pickle(output)
|
perf.to_pickle(output)
|
||||||
|
|
||||||
@@ -447,9 +509,7 @@ def live(ctx,
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
help='The name of the exchange bundle to ingest.',
|
||||||
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-f',
|
'-f',
|
||||||
@@ -485,30 +545,104 @@ def live(ctx,
|
|||||||
help='A list of symbols to exclude from the ingestion '
|
help='A list of symbols to exclude from the ingestion '
|
||||||
'(optional comma separated list)',
|
'(optional comma separated list)',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--csv',
|
||||||
|
default=None,
|
||||||
|
help='The path of a CSV file containing the data. If specified, start, '
|
||||||
|
'end, include-symbols and exclude-symbols will be ignored. Instead,'
|
||||||
|
'all data in the file will be ingested.',
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--show-progress/--no-show-progress',
|
'--show-progress/--no-show-progress',
|
||||||
default=True,
|
default=True,
|
||||||
help='Print progress information to the terminal.'
|
help='Print progress information to the terminal.'
|
||||||
)
|
)
|
||||||
def ingest_exchange(exchange_name, data_frequency, start, end,
|
@click.option(
|
||||||
include_symbols, exclude_symbols, show_progress):
|
'--verbose/--no-verbose`',
|
||||||
|
default=False,
|
||||||
|
help='Show a progress indicator for every currency pair.'
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--validate/--no-validate`',
|
||||||
|
default=False,
|
||||||
|
help='Report potential anomalies found in data bundles.'
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def ingest_exchange(ctx, exchange_name, data_frequency, start, end,
|
||||||
|
include_symbols, exclude_symbols, csv, show_progress,
|
||||||
|
verbose, validate):
|
||||||
"""
|
"""
|
||||||
Ingest data for the given exchange.
|
Ingest data for the given exchange.
|
||||||
"""
|
"""
|
||||||
exchange = get_exchange(exchange_name)
|
|
||||||
exchange_bundle = ExchangeBundle(exchange)
|
|
||||||
|
|
||||||
click.echo('Ingesting exchange bundle {}...'.format(exchange_name))
|
if exchange_name is None:
|
||||||
|
ctx.fail("must specify an exchange name '-x'")
|
||||||
|
|
||||||
|
exchange_bundle = ExchangeBundle(exchange_name)
|
||||||
|
|
||||||
|
click.echo('Trying to ingest exchange bundle {}...'.format(exchange_name),
|
||||||
|
sys.stdout)
|
||||||
exchange_bundle.ingest(
|
exchange_bundle.ingest(
|
||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
include_symbols=include_symbols,
|
include_symbols=include_symbols,
|
||||||
exclude_symbols=exclude_symbols,
|
exclude_symbols=exclude_symbols,
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
show_progress=show_progress
|
show_progress=show_progress,
|
||||||
|
show_breakdown=verbose,
|
||||||
|
show_report=validate,
|
||||||
|
csv=csv
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command(name='clean-algo')
|
||||||
|
@click.option(
|
||||||
|
'-n',
|
||||||
|
'--algo-namespace',
|
||||||
|
help='The label of the algorithm to for which to clean the state.'
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def clean_algo(ctx, algo_namespace):
|
||||||
|
click.echo(
|
||||||
|
'Cleaning algo state: {}'.format(algo_namespace),
|
||||||
|
sys.stdout
|
||||||
|
)
|
||||||
|
delete_algo_folder(algo_namespace)
|
||||||
|
click.echo('Done', sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command(name='clean-exchange')
|
||||||
|
@click.option(
|
||||||
|
'-x',
|
||||||
|
'--exchange-name',
|
||||||
|
help='The name of the exchange bundle to ingest.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'-f',
|
||||||
|
'--data-frequency',
|
||||||
|
type=click.Choice({'daily', 'minute'}),
|
||||||
|
default=None,
|
||||||
|
help='The bundle data frequency to remove. If not specified, it will '
|
||||||
|
'remove both daily and minute bundles.',
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def clean_exchange(ctx, exchange_name, data_frequency):
|
||||||
|
"""Clean up bundles from 'ingest-exchange'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if exchange_name is None:
|
||||||
|
ctx.fail("must specify an exchange name '-x'")
|
||||||
|
|
||||||
|
exchange_bundle = ExchangeBundle(exchange_name)
|
||||||
|
|
||||||
|
click.echo('Cleaning exchange bundle {}...'.format(exchange_name),
|
||||||
|
sys.stdout)
|
||||||
|
exchange_bundle.clean(
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
)
|
||||||
|
click.echo('Done', sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
'-b',
|
'-b',
|
||||||
@@ -521,9 +655,7 @@ def ingest_exchange(exchange_name, data_frequency, start, end,
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
help='The name of the exchange bundle to ingest.',
|
||||||
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-c',
|
'-c',
|
||||||
@@ -598,7 +730,7 @@ def ingest(ctx, bundle, exchange_name, compile_locally, assets_version,
|
|||||||
' This may not be passed with -e / --before or -a / --after',
|
' This may not be passed with -e / --before or -a / --after',
|
||||||
)
|
)
|
||||||
def clean(bundle, before, after, keep_last):
|
def clean(bundle, before, after, keep_last):
|
||||||
"""Clean up data downloaded with the ingest command.
|
"""Clean up bundles from 'ingest'.
|
||||||
"""
|
"""
|
||||||
bundles_module.clean(
|
bundles_module.clean(
|
||||||
bundle,
|
bundle,
|
||||||
@@ -629,7 +761,132 @@ def bundles():
|
|||||||
# because there were no entries, print a single message indicating that
|
# because there were no entries, print a single message indicating that
|
||||||
# no ingestions have yet been made.
|
# no ingestions have yet been made.
|
||||||
for timestamp in ingestions or ["<no ingestions>"]:
|
for timestamp in ingestions or ["<no ingestions>"]:
|
||||||
click.echo("%s %s" % (bundle, timestamp))
|
click.echo("%s %s" % (bundle, timestamp), sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
@main.group()
|
||||||
|
@click.pass_context
|
||||||
|
def marketplace(ctx):
|
||||||
|
"""Access the Enigma Data Marketplace to:\n
|
||||||
|
- Register and Publish new datasets (seller-side)\n
|
||||||
|
- Subscribe and Ingest premium datasets (buyer-side)\n
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.pass_context
|
||||||
|
def ls(ctx):
|
||||||
|
"""List all available datasets.
|
||||||
|
"""
|
||||||
|
click.echo('Listing of available data sources on the marketplace:',
|
||||||
|
sys.stdout)
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.list()
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.option(
|
||||||
|
'--dataset',
|
||||||
|
default=None,
|
||||||
|
help='The name of the dataset to ingest from the Data Marketplace.',
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def subscribe(ctx, dataset):
|
||||||
|
"""Subscribe to an existing dataset.
|
||||||
|
"""
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.subscribe(dataset)
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.option(
|
||||||
|
'--dataset',
|
||||||
|
default=None,
|
||||||
|
help='The name of the dataset to ingest from the Data Marketplace.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'-f',
|
||||||
|
'--data-frequency',
|
||||||
|
type=click.Choice({'daily', 'minute', 'daily,minute', 'minute,daily'}),
|
||||||
|
default='daily',
|
||||||
|
show_default=True,
|
||||||
|
help='The data frequency of the desired OHLCV bars.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'-s',
|
||||||
|
'--start',
|
||||||
|
default=None,
|
||||||
|
type=Date(tz='utc', as_timestamp=True),
|
||||||
|
help='The start date of the data range. (default: one year from end date)',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'-e',
|
||||||
|
'--end',
|
||||||
|
default=None,
|
||||||
|
type=Date(tz='utc', as_timestamp=True),
|
||||||
|
help='The end date of the data range. (default: today)',
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def ingest(ctx, dataset, data_frequency, start, end):
|
||||||
|
"""Ingest a dataset (requires subscription).
|
||||||
|
"""
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.ingest(dataset, data_frequency, start, end)
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.option(
|
||||||
|
'--dataset',
|
||||||
|
default=None,
|
||||||
|
help='The name of the dataset to ingest from the Data Marketplace.',
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def clean(ctx, dataset):
|
||||||
|
"""Clean/Remove local data for a given dataset.
|
||||||
|
"""
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.clean(dataset)
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.pass_context
|
||||||
|
def register(ctx):
|
||||||
|
"""Register a new dataset.
|
||||||
|
"""
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.register()
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.option(
|
||||||
|
'--dataset',
|
||||||
|
default=None,
|
||||||
|
help='The name of the Marketplace dataset to publish data for.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--datadir',
|
||||||
|
default=None,
|
||||||
|
help='The folder that contains the CSV data files to publish.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--watch/--no-watch',
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help='Whether to watch the datadir for live data.',
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def publish(ctx, dataset, datadir, watch):
|
||||||
|
"""Publish data for a registered dataset.
|
||||||
|
"""
|
||||||
|
marketplace = Marketplace()
|
||||||
|
if dataset is None:
|
||||||
|
ctx.fail("must specify a dataset to publish data for "
|
||||||
|
" with '--dataset'\n")
|
||||||
|
if datadir is None:
|
||||||
|
ctx.fail("must specify a datadir where to find the files to publish "
|
||||||
|
" with '--datadir'\n")
|
||||||
|
marketplace.publish(dataset, datadir, watch)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import warnings
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from pandas.tslib import normalize_date
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@@ -564,7 +563,7 @@ cdef class BarData:
|
|||||||
})
|
})
|
||||||
|
|
||||||
cdef bool _is_stale_for_asset(self, asset, dt, adjusted_dt, data_portal):
|
cdef bool _is_stale_for_asset(self, asset, dt, adjusted_dt, data_portal):
|
||||||
session_label = normalize_date(dt) # FIXME
|
session_label = dt.normalize_date() # FIXME
|
||||||
|
|
||||||
if not asset.is_alive_for_session(session_label):
|
if not asset.is_alive_for_session(session_label):
|
||||||
return False
|
return False
|
||||||
|
|||||||
+11
-12
@@ -21,7 +21,6 @@ import logbook
|
|||||||
import pytz
|
import pytz
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from contextlib2 import ExitStack
|
from contextlib2 import ExitStack
|
||||||
from pandas.tseries.tools import normalize_date
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from itertools import chain, repeat
|
from itertools import chain, repeat
|
||||||
@@ -124,7 +123,6 @@ from catalyst.utils.events import (
|
|||||||
from catalyst.utils.factory import create_simulation_parameters
|
from catalyst.utils.factory import create_simulation_parameters
|
||||||
from catalyst.utils.math_utils import (
|
from catalyst.utils.math_utils import (
|
||||||
tolerant_equals,
|
tolerant_equals,
|
||||||
round_if_near_integer,
|
|
||||||
round_nearest
|
round_nearest
|
||||||
)
|
)
|
||||||
from catalyst.utils.pandas_utils import clear_dataframe_indexer_caches
|
from catalyst.utils.pandas_utils import clear_dataframe_indexer_caches
|
||||||
@@ -138,8 +136,9 @@ from catalyst.gens.sim_engine import MinuteSimulationClock
|
|||||||
from catalyst.sources.benchmark_source import BenchmarkSource
|
from catalyst.sources.benchmark_source import BenchmarkSource
|
||||||
from catalyst.catalyst_warnings import ZiplineDeprecationWarning
|
from catalyst.catalyst_warnings import ZiplineDeprecationWarning
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
log = logbook.Logger("ZiplineLog")
|
log = logbook.Logger("CatalystLog", level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class TradingAlgorithm(object):
|
class TradingAlgorithm(object):
|
||||||
@@ -939,7 +938,7 @@ class TradingAlgorithm(object):
|
|||||||
The field to query. The options have the following meanings:
|
The field to query. The options have the following meanings:
|
||||||
arena : str
|
arena : str
|
||||||
The arena from the simulation parameters. This will normally
|
The arena from the simulation parameters. This will normally
|
||||||
be ``'backtest'`` but some systems may use this distinguish
|
be ``backtest`` but some systems may use this distinguish
|
||||||
live trading from backtesting.
|
live trading from backtesting.
|
||||||
data_frequency : {'daily', 'minute'}
|
data_frequency : {'daily', 'minute'}
|
||||||
data_frequency tells the algorithm if it is running with
|
data_frequency tells the algorithm if it is running with
|
||||||
@@ -954,7 +953,7 @@ class TradingAlgorithm(object):
|
|||||||
The platform that the code is running on. By default this
|
The platform that the code is running on. By default this
|
||||||
will be the string 'catalyst'. This can allow algorithms to
|
will be the string 'catalyst'. This can allow algorithms to
|
||||||
know if they are running on the Quantopian platform instead.
|
know if they are running on the Quantopian platform instead.
|
||||||
* : dict[str -> any]
|
\* : dict[str -> any]
|
||||||
Returns all of the fields in a dictionary.
|
Returns all of the fields in a dictionary.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -1032,7 +1031,7 @@ class TradingAlgorithm(object):
|
|||||||
argument is the name of the column in the preprocessed dataframe
|
argument is the name of the column in the preprocessed dataframe
|
||||||
containing the symbols. This will be used along with the date
|
containing the symbols. This will be used along with the date
|
||||||
information to map the sids in the asset finder.
|
information to map the sids in the asset finder.
|
||||||
**kwargs
|
\*\*kwargs
|
||||||
Forwarded to :func:`pandas.read_csv`.
|
Forwarded to :func:`pandas.read_csv`.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -1156,7 +1155,7 @@ class TradingAlgorithm(object):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
**kwargs
|
\*\*kwargs
|
||||||
The names and values to record.
|
The names and values to record.
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
@@ -1273,7 +1272,7 @@ class TradingAlgorithm(object):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
*args : iterable[str]
|
\*args : iterable[str]
|
||||||
The ticker symbols to lookup.
|
The ticker symbols to lookup.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
@@ -1345,7 +1344,7 @@ class TradingAlgorithm(object):
|
|||||||
# Make sure the asset exists, and that there is a last price for it.
|
# Make sure the asset exists, and that there is a last price for it.
|
||||||
# FIXME: we should use BarData's can_trade logic here, but I haven't
|
# FIXME: we should use BarData's can_trade logic here, but I haven't
|
||||||
# yet found a good way to do that.
|
# yet found a good way to do that.
|
||||||
normalized_date = normalize_date(self.datetime)
|
normalized_date = self.datetime.normalize()
|
||||||
|
|
||||||
if normalized_date < asset.start_date:
|
if normalized_date < asset.start_date:
|
||||||
raise CannotOrderDelistedAsset(
|
raise CannotOrderDelistedAsset(
|
||||||
@@ -1392,7 +1391,7 @@ class TradingAlgorithm(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if asset.auto_close_date:
|
if asset.auto_close_date:
|
||||||
day = normalize_date(self.get_datetime())
|
day = self.get_datetime().normalize()
|
||||||
|
|
||||||
if day > min(asset.end_date, asset.auto_close_date):
|
if day > min(asset.end_date, asset.auto_close_date):
|
||||||
# If we are after the asset's end date or auto close date, warn
|
# If we are after the asset's end date or auto close date, warn
|
||||||
@@ -1484,7 +1483,6 @@ class TradingAlgorithm(object):
|
|||||||
"""
|
"""
|
||||||
Converts the number of shares to the smallest tradable lot size for
|
Converts the number of shares to the smallest tradable lot size for
|
||||||
the asset being ordered.
|
the asset being ordered.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return round_nearest(amount, asset.min_trade_size)
|
return round_nearest(amount, asset.min_trade_size)
|
||||||
|
|
||||||
@@ -1522,6 +1520,7 @@ class TradingAlgorithm(object):
|
|||||||
self.updated_portfolio(),
|
self.updated_portfolio(),
|
||||||
self.get_datetime(),
|
self.get_datetime(),
|
||||||
self.trading_client.current_data)
|
self.trading_client.current_data)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
||||||
"""
|
"""
|
||||||
@@ -2475,7 +2474,7 @@ class TradingAlgorithm(object):
|
|||||||
"""
|
"""
|
||||||
Internal implementation of `pipeline_output`.
|
Internal implementation of `pipeline_output`.
|
||||||
"""
|
"""
|
||||||
today = normalize_date(self.get_datetime())
|
today = self.get_datetime().normalize()
|
||||||
data = NO_DATA = object()
|
data = NO_DATA = object()
|
||||||
try:
|
try:
|
||||||
data = self._pipeline_cache.unwrap(today)
|
data = self._pipeline_cache.unwrap(today)
|
||||||
|
|||||||
+65
-8
@@ -34,6 +34,7 @@ def attach_pipeline(pipeline, name, chunks=None):
|
|||||||
:func:`catalyst.api.pipeline_output`
|
:func:`catalyst.api.pipeline_output`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def batch_market_order(share_counts):
|
def batch_market_order(share_counts):
|
||||||
"""Place a batch market order for multiple assets.
|
"""Place a batch market order for multiple assets.
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ def batch_market_order(share_counts):
|
|||||||
Index of ids for newly-created orders.
|
Index of ids for newly-created orders.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def cancel_order(order_param):
|
def cancel_order(order_param):
|
||||||
"""Cancel an open order.
|
"""Cancel an open order.
|
||||||
|
|
||||||
@@ -57,7 +59,9 @@ def cancel_order(order_param):
|
|||||||
The order_id or order object to cancel.
|
The order_id or order object to cancel.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def continuous_future(root_symbol_str, offset=0, roll='volume', adjustment='mul'):
|
|
||||||
|
def continuous_future(root_symbol_str, offset=0, roll='volume',
|
||||||
|
adjustment='mul'):
|
||||||
"""Create a specifier for a continuous contract.
|
"""Create a specifier for a continuous contract.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -81,7 +85,10 @@ def continuous_future(root_symbol_str, offset=0, roll='volume', adjustment='mul'
|
|||||||
The continuous future specifier.
|
The continuous future specifier.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def fetch_csv(url, pre_func=None, post_func=None, date_column='date', date_format=None, timezone='UTC', symbol=None, mask=True, symbol_column=None, special_params_checker=None, **kwargs):
|
|
||||||
|
def fetch_csv(url, pre_func=None, post_func=None, date_column='date',
|
||||||
|
date_format=None, timezone='UTC', symbol=None, mask=True,
|
||||||
|
symbol_column=None, special_params_checker=None, **kwargs):
|
||||||
"""Fetch a csv from a remote url and register the data so that it is
|
"""Fetch a csv from a remote url and register the data so that it is
|
||||||
queryable from the ``data`` object.
|
queryable from the ``data`` object.
|
||||||
|
|
||||||
@@ -125,6 +132,7 @@ def fetch_csv(url, pre_func=None, post_func=None, date_column='date', date_forma
|
|||||||
A requests source that will pull data from the url specified.
|
A requests source that will pull data from the url specified.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def future_symbol(symbol):
|
def future_symbol(symbol):
|
||||||
"""Lookup a futures contract with a given symbol.
|
"""Lookup a futures contract with a given symbol.
|
||||||
|
|
||||||
@@ -144,6 +152,7 @@ def future_symbol(symbol):
|
|||||||
Raised when no contract named 'symbol' is found.
|
Raised when no contract named 'symbol' is found.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_datetime(tz=None):
|
def get_datetime(tz=None):
|
||||||
"""
|
"""
|
||||||
Returns the current simulation datetime.
|
Returns the current simulation datetime.
|
||||||
@@ -159,6 +168,7 @@ dt : datetime
|
|||||||
The current simulation datetime converted to ``tz``.
|
The current simulation datetime converted to ``tz``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_environment(field='platform'):
|
def get_environment(field='platform'):
|
||||||
"""Query the execution environment.
|
"""Query the execution environment.
|
||||||
|
|
||||||
@@ -198,6 +208,7 @@ def get_environment(field='platform'):
|
|||||||
Raised when ``field`` is not a valid option.
|
Raised when ``field`` is not a valid option.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_order(order_id):
|
def get_order(order_id):
|
||||||
"""Lookup an order based on the order id returned from one of the
|
"""Lookup an order based on the order id returned from one of the
|
||||||
order functions.
|
order functions.
|
||||||
@@ -213,10 +224,12 @@ def get_order(order_id):
|
|||||||
The order object.
|
The order object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def history(bar_count, frequency, field, ffill=True):
|
def history(bar_count, frequency, field, ffill=True):
|
||||||
"""DEPRECATED: use ``data.history`` instead.
|
"""DEPRECATED: use ``data.history`` instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def order(asset, amount, limit_price=None, stop_price=None, style=None):
|
def order(asset, amount, limit_price=None, stop_price=None, style=None):
|
||||||
"""Place an order.
|
"""Place an order.
|
||||||
|
|
||||||
@@ -258,7 +271,9 @@ def order(asset, amount, limit_price=None, stop_price=None, style=None):
|
|||||||
:func:`catalyst.api.order_percent`
|
:func:`catalyst.api.order_percent`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def order_percent(asset, percent, limit_price=None, stop_price=None, style=None):
|
|
||||||
|
def order_percent(asset, percent, limit_price=None, stop_price=None,
|
||||||
|
style=None):
|
||||||
"""Place an order in the specified asset corresponding to the given
|
"""Place an order in the specified asset corresponding to the given
|
||||||
percent of the current portfolio value.
|
percent of the current portfolio value.
|
||||||
|
|
||||||
@@ -293,6 +308,7 @@ def order_percent(asset, percent, limit_price=None, stop_price=None, style=None)
|
|||||||
:func:`catalyst.api.order_value`
|
:func:`catalyst.api.order_value`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def order_target(asset, target, limit_price=None, stop_price=None, style=None):
|
def order_target(asset, target, limit_price=None, stop_price=None, style=None):
|
||||||
"""Place an order to adjust a position to a target number of shares. If
|
"""Place an order to adjust a position to a target number of shares. If
|
||||||
the position doesn't already exist, this is equivalent to placing a new
|
the position doesn't already exist, this is equivalent to placing a new
|
||||||
@@ -344,7 +360,9 @@ def order_target(asset, target, limit_price=None, stop_price=None, style=None):
|
|||||||
:func:`catalyst.api.order_target_value`
|
:func:`catalyst.api.order_target_value`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def order_target_percent(asset, target, limit_price=None, stop_price=None, style=None):
|
|
||||||
|
def order_target_percent(asset, target, limit_price=None, stop_price=None,
|
||||||
|
style=None):
|
||||||
"""Place an order to adjust a position to a target percent of the
|
"""Place an order to adjust a position to a target percent of the
|
||||||
current portfolio value. If the position doesn't already exist, this is
|
current portfolio value. If the position doesn't already exist, this is
|
||||||
equivalent to placing a new order. If the position does exist, this is
|
equivalent to placing a new order. If the position does exist, this is
|
||||||
@@ -396,7 +414,9 @@ def order_target_percent(asset, target, limit_price=None, stop_price=None, style
|
|||||||
:func:`catalyst.api.order_target_value`
|
:func:`catalyst.api.order_target_value`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def order_target_value(asset, target, limit_price=None, stop_price=None, style=None):
|
|
||||||
|
def order_target_value(asset, target, limit_price=None, stop_price=None,
|
||||||
|
style=None):
|
||||||
"""Place an order to adjust a position to a target value. If
|
"""Place an order to adjust a position to a target value. If
|
||||||
the position doesn't already exist, this is equivalent to placing a new
|
the position doesn't already exist, this is equivalent to placing a new
|
||||||
order. If the position does exist, this is equivalent to placing an
|
order. If the position does exist, this is equivalent to placing an
|
||||||
@@ -448,6 +468,7 @@ def order_target_value(asset, target, limit_price=None, stop_price=None, style=N
|
|||||||
:func:`catalyst.api.order_target_percent`
|
:func:`catalyst.api.order_target_percent`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def order_value(asset, value, limit_price=None, stop_price=None, style=None):
|
def order_value(asset, value, limit_price=None, stop_price=None, style=None):
|
||||||
"""Place an order by desired value rather than desired number of
|
"""Place an order by desired value rather than desired number of
|
||||||
shares.
|
shares.
|
||||||
@@ -488,6 +509,7 @@ def order_value(asset, value, limit_price=None, stop_price=None, style=None):
|
|||||||
:func:`catalyst.api.order_percent`
|
:func:`catalyst.api.order_percent`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pipeline_output(name):
|
def pipeline_output(name):
|
||||||
"""Get the results of the pipeline that was attached with the name:
|
"""Get the results of the pipeline that was attached with the name:
|
||||||
``name``.
|
``name``.
|
||||||
@@ -514,6 +536,7 @@ def pipeline_output(name):
|
|||||||
:meth:`catalyst.pipeline.engine.PipelineEngine.run_pipeline`
|
:meth:`catalyst.pipeline.engine.PipelineEngine.run_pipeline`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def record(*args, **kwargs):
|
def record(*args, **kwargs):
|
||||||
"""Track and record values each day.
|
"""Track and record values each day.
|
||||||
|
|
||||||
@@ -529,7 +552,9 @@ def record(*args, **kwargs):
|
|||||||
:func:`~catalyst.run_algorithm`.
|
:func:`~catalyst.run_algorithm`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def schedule_function(func, date_rule=None, time_rule=None, half_days=True, calendar=None):
|
|
||||||
|
def schedule_function(func, date_rule=None, time_rule=None, half_days=True,
|
||||||
|
calendar=None):
|
||||||
"""Schedules a function to be called according to some timed rules.
|
"""Schedules a function to be called according to some timed rules.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -549,6 +574,7 @@ def schedule_function(func, date_rule=None, time_rule=None, half_days=True, cale
|
|||||||
:class:`catalyst.api.time_rules`
|
:class:`catalyst.api.time_rules`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_asset_restrictions(restrictions, on_error='fail'):
|
def set_asset_restrictions(restrictions, on_error='fail'):
|
||||||
"""Set a restriction on which assets can be ordered.
|
"""Set a restriction on which assets can be ordered.
|
||||||
|
|
||||||
@@ -562,6 +588,7 @@ def set_asset_restrictions(restrictions, on_error='fail'):
|
|||||||
catalyst.finance.asset_restrictions.Restrictions
|
catalyst.finance.asset_restrictions.Restrictions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_benchmark(benchmark):
|
def set_benchmark(benchmark):
|
||||||
"""Set the benchmark asset.
|
"""Set the benchmark asset.
|
||||||
|
|
||||||
@@ -576,6 +603,7 @@ def set_benchmark(benchmark):
|
|||||||
automatically reinvested.
|
automatically reinvested.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_cancel_policy(cancel_policy):
|
def set_cancel_policy(cancel_policy):
|
||||||
"""Sets the order cancellation policy for the simulation.
|
"""Sets the order cancellation policy for the simulation.
|
||||||
|
|
||||||
@@ -590,6 +618,7 @@ def set_cancel_policy(cancel_policy):
|
|||||||
:class:`catalyst.api.NeverCancel`
|
:class:`catalyst.api.NeverCancel`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_commission(commission):
|
def set_commission(commission):
|
||||||
"""Sets the commission model for the simulation.
|
"""Sets the commission model for the simulation.
|
||||||
|
|
||||||
@@ -605,6 +634,7 @@ def set_commission(commission):
|
|||||||
:class:`catalyst.finance.commission.PerDollar`
|
:class:`catalyst.finance.commission.PerDollar`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_do_not_order_list(restricted_list, on_error='fail'):
|
def set_do_not_order_list(restricted_list, on_error='fail'):
|
||||||
"""Set a restriction on which assets can be ordered.
|
"""Set a restriction on which assets can be ordered.
|
||||||
|
|
||||||
@@ -614,11 +644,13 @@ def set_do_not_order_list(restricted_list, on_error='fail'):
|
|||||||
The assets that cannot be ordered.
|
The assets that cannot be ordered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_long_only(on_error='fail'):
|
def set_long_only(on_error='fail'):
|
||||||
"""Set a rule specifying that this algorithm cannot take short
|
"""Set a rule specifying that this algorithm cannot take short
|
||||||
positions.
|
positions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_max_leverage(max_leverage):
|
def set_max_leverage(max_leverage):
|
||||||
"""Set a limit on the maximum leverage of the algorithm.
|
"""Set a limit on the maximum leverage of the algorithm.
|
||||||
|
|
||||||
@@ -629,6 +661,7 @@ def set_max_leverage(max_leverage):
|
|||||||
be no maximum.
|
be no maximum.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_max_order_count(max_count, on_error='fail'):
|
def set_max_order_count(max_count, on_error='fail'):
|
||||||
"""Set a limit on the number of orders that can be placed in a single
|
"""Set a limit on the number of orders that can be placed in a single
|
||||||
day.
|
day.
|
||||||
@@ -639,7 +672,9 @@ def set_max_order_count(max_count, on_error='fail'):
|
|||||||
The maximum number of orders that can be placed on any single day.
|
The maximum number of orders that can be placed on any single day.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_max_order_size(asset=None, max_shares=None, max_notional=None, on_error='fail'):
|
|
||||||
|
def set_max_order_size(asset=None, max_shares=None, max_notional=None,
|
||||||
|
on_error='fail'):
|
||||||
"""Set a limit on the number of shares and/or dollar value of any single
|
"""Set a limit on the number of shares and/or dollar value of any single
|
||||||
order placed for sid. Limits are treated as absolute values and are
|
order placed for sid. Limits are treated as absolute values and are
|
||||||
enforced at the time that the algo attempts to place an order for sid.
|
enforced at the time that the algo attempts to place an order for sid.
|
||||||
@@ -658,7 +693,9 @@ def set_max_order_size(asset=None, max_shares=None, max_notional=None, on_error=
|
|||||||
The maximum value that can be ordered at one time.
|
The maximum value that can be ordered at one time.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_max_position_size(asset=None, max_shares=None, max_notional=None, on_error='fail'):
|
|
||||||
|
def set_max_position_size(asset=None, max_shares=None, max_notional=None,
|
||||||
|
on_error='fail'):
|
||||||
"""Set a limit on the number of shares and/or dollar value held for the
|
"""Set a limit on the number of shares and/or dollar value held for the
|
||||||
given sid. Limits are treated as absolute values and are enforced at
|
given sid. Limits are treated as absolute values and are enforced at
|
||||||
the time that the algo attempts to place an order for sid. This means
|
the time that the algo attempts to place an order for sid. This means
|
||||||
@@ -681,6 +718,7 @@ def set_max_position_size(asset=None, max_shares=None, max_notional=None, on_err
|
|||||||
The maximum value to hold for an asset.
|
The maximum value to hold for an asset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_slippage(slippage):
|
def set_slippage(slippage):
|
||||||
"""Set the slippage model for the simulation.
|
"""Set the slippage model for the simulation.
|
||||||
|
|
||||||
@@ -694,6 +732,7 @@ def set_slippage(slippage):
|
|||||||
:class:`catalyst.finance.slippage.SlippageModel`
|
:class:`catalyst.finance.slippage.SlippageModel`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_symbol_lookup_date(dt):
|
def set_symbol_lookup_date(dt):
|
||||||
"""Set the date for which symbols will be resolved to their assets
|
"""Set the date for which symbols will be resolved to their assets
|
||||||
(symbols may map to different firms or underlying assets at
|
(symbols may map to different firms or underlying assets at
|
||||||
@@ -705,6 +744,7 @@ def set_symbol_lookup_date(dt):
|
|||||||
The new symbol lookup date.
|
The new symbol lookup date.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def sid(sid):
|
def sid(sid):
|
||||||
"""Lookup an Asset by its unique asset identifier.
|
"""Lookup an Asset by its unique asset identifier.
|
||||||
|
|
||||||
@@ -724,6 +764,7 @@ def sid(sid):
|
|||||||
When a requested ``sid`` does not map to any asset.
|
When a requested ``sid`` does not map to any asset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def symbol(symbol_str):
|
def symbol(symbol_str):
|
||||||
"""Lookup an Equity by its ticker symbol.
|
"""Lookup an Equity by its ticker symbol.
|
||||||
|
|
||||||
@@ -748,6 +789,7 @@ def symbol(symbol_str):
|
|||||||
:func:`catalyst.api.set_symbol_lookup_date`
|
:func:`catalyst.api.set_symbol_lookup_date`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def symbols(*args):
|
def symbols(*args):
|
||||||
"""Lookup multuple Equities as a list.
|
"""Lookup multuple Equities as a list.
|
||||||
|
|
||||||
@@ -773,3 +815,18 @@ def symbols(*args):
|
|||||||
:func:`catalyst.api.set_symbol_lookup_date`
|
:func:`catalyst.api.set_symbol_lookup_date`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_dataset(ds_name, start=None, end=None):
|
||||||
|
"""
|
||||||
|
Lookup a data source from the marketplace
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds_name: str
|
||||||
|
start: pd.Timestamp
|
||||||
|
end: pd.Timestamp
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
|||||||
+95
-14
@@ -17,6 +17,9 @@
|
|||||||
"""
|
"""
|
||||||
Cythonized Asset object.
|
Cythonized Asset object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
cimport cython
|
cimport cython
|
||||||
from cpython.number cimport PyNumber_Index
|
from cpython.number cimport PyNumber_Index
|
||||||
from cpython.object cimport (
|
from cpython.object cimport (
|
||||||
@@ -36,6 +39,7 @@ from numpy cimport int64_t
|
|||||||
import warnings
|
import warnings
|
||||||
cimport numpy as np
|
cimport numpy as np
|
||||||
|
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_sid
|
||||||
from catalyst.utils.calendars import get_calendar
|
from catalyst.utils.calendars import get_calendar
|
||||||
from catalyst.exchange.exchange_errors import InvalidSymbolError, SidHashError
|
from catalyst.exchange.exchange_errors import InvalidSymbolError, SidHashError
|
||||||
|
|
||||||
@@ -393,11 +397,18 @@ cdef class Future(Asset):
|
|||||||
|
|
||||||
cdef class TradingPair(Asset):
|
cdef class TradingPair(Asset):
|
||||||
cdef readonly float leverage
|
cdef readonly float leverage
|
||||||
cdef readonly object market_currency
|
cdef readonly object quote_currency
|
||||||
cdef readonly object base_currency
|
cdef readonly object base_currency
|
||||||
cdef readonly object end_daily
|
cdef readonly object end_daily
|
||||||
cdef readonly object end_minute
|
cdef readonly object end_minute
|
||||||
cdef readonly object exchange_symbol
|
cdef readonly object exchange_symbol
|
||||||
|
cdef readonly float maker
|
||||||
|
cdef readonly float taker
|
||||||
|
cdef readonly int trading_state
|
||||||
|
cdef readonly object data_source
|
||||||
|
cdef readonly float max_trade_size
|
||||||
|
cdef readonly float lot
|
||||||
|
cdef readonly int decimals
|
||||||
|
|
||||||
_kwargnames = frozenset({
|
_kwargnames = frozenset({
|
||||||
'sid',
|
'sid',
|
||||||
@@ -410,12 +421,19 @@ cdef class TradingPair(Asset):
|
|||||||
'exchange',
|
'exchange',
|
||||||
'exchange_full',
|
'exchange_full',
|
||||||
'leverage',
|
'leverage',
|
||||||
'market_currency',
|
'quote_currency',
|
||||||
'base_currency',
|
'base_currency',
|
||||||
'end_daily',
|
'end_daily',
|
||||||
'end_minute',
|
'end_minute',
|
||||||
'exchange_symbol',
|
'exchange_symbol',
|
||||||
'min_trade_size'
|
'min_trade_size',
|
||||||
|
'max_trade_size',
|
||||||
|
'lot',
|
||||||
|
'maker',
|
||||||
|
'taker',
|
||||||
|
'trading_state',
|
||||||
|
'data_source',
|
||||||
|
'decimals'
|
||||||
})
|
})
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
object symbol,
|
object symbol,
|
||||||
@@ -431,10 +449,17 @@ cdef class TradingPair(Asset):
|
|||||||
object first_traded=None,
|
object first_traded=None,
|
||||||
object auto_close_date=None,
|
object auto_close_date=None,
|
||||||
object exchange_full=None,
|
object exchange_full=None,
|
||||||
object min_trade_size=None):
|
float min_trade_size=0.0001,
|
||||||
|
float max_trade_size=1000000,
|
||||||
|
float maker=0.0015,
|
||||||
|
float taker=0.0025,
|
||||||
|
float lot=0,
|
||||||
|
int decimals = 8,
|
||||||
|
int trading_state=0,
|
||||||
|
object data_source='catalyst'):
|
||||||
"""
|
"""
|
||||||
Replicates the Asset constructor with some built-in conventions
|
Replicates the Asset constructor with some built-in conventions
|
||||||
and a new 'leverage' attribute.
|
and adds properties for leverage and fees.
|
||||||
|
|
||||||
Symbol
|
Symbol
|
||||||
------
|
------
|
||||||
@@ -466,8 +491,6 @@ cdef class TradingPair(Asset):
|
|||||||
highest volume and market cap generally benefit from high leverage.
|
highest volume and market cap generally benefit from high leverage.
|
||||||
New currencies from ICO generally cannot be leveraged.
|
New currencies from ICO generally cannot be leveraged.
|
||||||
|
|
||||||
The leverage value is either None or and integer.
|
|
||||||
|
|
||||||
Leverage allows you to open a larger position with a smaller amount
|
Leverage allows you to open a larger position with a smaller amount
|
||||||
of funds. For example, if you open a $5,000 position in BTC/USD
|
of funds. For example, if you open a $5,000 position in BTC/USD
|
||||||
with 5:1 leverage, only one-fifth of this amount, or $1000, will be
|
with 5:1 leverage, only one-fifth of this amount, or $1000, will be
|
||||||
@@ -477,6 +500,11 @@ cdef class TradingPair(Asset):
|
|||||||
the position. If you open with 1:1 leverage, $5,000 of your balance
|
the position. If you open with 1:1 leverage, $5,000 of your balance
|
||||||
will be tied to the position.
|
will be tied to the position.
|
||||||
|
|
||||||
|
Fees
|
||||||
|
----
|
||||||
|
Exchanges generally charge a taker (taking from the order book) or
|
||||||
|
maker (adding to the order book) fee.
|
||||||
|
|
||||||
:param symbol:
|
:param symbol:
|
||||||
:param exchange:
|
:param exchange:
|
||||||
:param start_date:
|
:param start_date:
|
||||||
@@ -491,17 +519,23 @@ cdef class TradingPair(Asset):
|
|||||||
:param auto_close_date:
|
:param auto_close_date:
|
||||||
:param exchange_full:
|
:param exchange_full:
|
||||||
:param min_trade_size:
|
:param min_trade_size:
|
||||||
|
:param max_trade_size:
|
||||||
|
:param maker:
|
||||||
|
:param taker:
|
||||||
|
:param data_source
|
||||||
|
:param decimals
|
||||||
|
:param lot
|
||||||
"""
|
"""
|
||||||
|
|
||||||
symbol = symbol.lower()
|
symbol = symbol.lower()
|
||||||
try:
|
try:
|
||||||
self.market_currency, self.base_currency = symbol.split('_')
|
self.base_currency, self.quote_currency = symbol.split('_')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise InvalidSymbolError(symbol=symbol, error=e)
|
raise InvalidSymbolError(symbol=symbol, error=e)
|
||||||
|
|
||||||
if sid == 0 or sid is None:
|
if sid == 0 or sid is None:
|
||||||
try:
|
try:
|
||||||
sid = abs(hash(symbol)) % (10 ** 4)
|
sid = get_sid(symbol)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise SidHashError(symbol=symbol)
|
raise SidHashError(symbol=symbol)
|
||||||
|
|
||||||
@@ -509,11 +543,14 @@ cdef class TradingPair(Asset):
|
|||||||
asset_name = ' / '.join(symbol.split('_')).upper()
|
asset_name = ' / '.join(symbol.split('_')).upper()
|
||||||
|
|
||||||
if start_date is None:
|
if start_date is None:
|
||||||
start_date = pd.Timestamp.utcnow()
|
start_date = pd.to_datetime('2009-1-1', utc=True)
|
||||||
|
|
||||||
if end_date is None:
|
if end_date is None:
|
||||||
end_date = pd.Timestamp.utcnow() + timedelta(days=365)
|
end_date = pd.Timestamp.utcnow() + timedelta(days=365)
|
||||||
|
|
||||||
|
if lot == 0 and min_trade_size > 0:
|
||||||
|
lot = min_trade_size
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
sid,
|
sid,
|
||||||
exchange,
|
exchange,
|
||||||
@@ -524,19 +561,26 @@ cdef class TradingPair(Asset):
|
|||||||
first_traded=first_traded,
|
first_traded=first_traded,
|
||||||
auto_close_date=auto_close_date,
|
auto_close_date=auto_close_date,
|
||||||
exchange_full=exchange_full,
|
exchange_full=exchange_full,
|
||||||
min_trade_size=min_trade_size
|
min_trade_size=min_trade_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.maker = maker
|
||||||
|
self.taker = taker
|
||||||
self.leverage = leverage
|
self.leverage = leverage
|
||||||
self.end_daily = end_daily
|
self.end_daily = end_daily
|
||||||
self.end_minute = end_minute
|
self.end_minute = end_minute
|
||||||
self.exchange_symbol = exchange_symbol
|
self.exchange_symbol = exchange_symbol
|
||||||
|
self.trading_state = trading_state
|
||||||
|
self.data_source = data_source
|
||||||
|
self.max_trade_size = max_trade_size
|
||||||
|
self.lot = lot
|
||||||
|
self.decimals = decimals
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \
|
return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \
|
||||||
'Introduced On: {start_date}, ' \
|
'Introduced On: {start_date}, ' \
|
||||||
'Market Currency: {market_currency}, ' \
|
|
||||||
'Base Currency: {base_currency}, ' \
|
'Base Currency: {base_currency}, ' \
|
||||||
|
'Quote Currency: {quote_currency}, ' \
|
||||||
'Exchange Leverage: {leverage}, ' \
|
'Exchange Leverage: {leverage}, ' \
|
||||||
'Minimum Trade Size: {min_trade_size} ' \
|
'Minimum Trade Size: {min_trade_size} ' \
|
||||||
'Last daily ingestion: {end_daily} ' \
|
'Last daily ingestion: {end_daily} ' \
|
||||||
@@ -545,7 +589,7 @@ cdef class TradingPair(Asset):
|
|||||||
sid=self.sid,
|
sid=self.sid,
|
||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
start_date=self.start_date,
|
start_date=self.start_date,
|
||||||
market_currency=self.market_currency,
|
quote_currency=self.quote_currency,
|
||||||
base_currency=self.base_currency,
|
base_currency=self.base_currency,
|
||||||
leverage=self.leverage,
|
leverage=self.leverage,
|
||||||
min_trade_size=self.min_trade_size,
|
min_trade_size=self.min_trade_size,
|
||||||
@@ -553,6 +597,32 @@ cdef class TradingPair(Asset):
|
|||||||
end_minute=self.end_minute
|
end_minute=self.end_minute
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cpdef to_dict(self):
|
||||||
|
"""
|
||||||
|
Convert to a python dict.
|
||||||
|
"""
|
||||||
|
#TODO: missing fields
|
||||||
|
super_dict = super(TradingPair, self).to_dict()
|
||||||
|
super_dict['end_daily'] = self.end_daily
|
||||||
|
super_dict['end_minute'] = self.end_minute
|
||||||
|
super_dict['leverage'] = self.leverage
|
||||||
|
super_dict['min_trade_size'] = self.min_trade_size
|
||||||
|
return super_dict
|
||||||
|
|
||||||
|
def is_exchange_open(self, dt_minute):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dt_minute: pd.Timestamp (UTC, tz-aware)
|
||||||
|
The minute to check.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
boolean: whether the asset's exchange is open at the given minute.
|
||||||
|
"""
|
||||||
|
#TODO: make more dymanic to catch holds
|
||||||
|
return True
|
||||||
|
|
||||||
cpdef __reduce__(self):
|
cpdef __reduce__(self):
|
||||||
"""
|
"""
|
||||||
Function used by pickle to determine how to serialize/deserialize this
|
Function used by pickle to determine how to serialize/deserialize this
|
||||||
@@ -560,17 +630,28 @@ cdef class TradingPair(Asset):
|
|||||||
and whose second element is a tuple of all the attributes that should
|
and whose second element is a tuple of all the attributes that should
|
||||||
be serialized/deserialized during pickling.
|
be serialized/deserialized during pickling.
|
||||||
"""
|
"""
|
||||||
|
# added arguments for catalyst
|
||||||
return (self.__class__, (self.symbol,
|
return (self.__class__, (self.symbol,
|
||||||
self.exchange,
|
self.exchange,
|
||||||
self.start_date,
|
self.start_date,
|
||||||
self.asset_name,
|
self.asset_name,
|
||||||
self.sid,
|
self.sid,
|
||||||
self.leverage,
|
self.leverage,
|
||||||
|
self.end_daily,
|
||||||
|
self.end_minute,
|
||||||
self.end_date,
|
self.end_date,
|
||||||
|
self.exchange_symbol,
|
||||||
self.first_traded,
|
self.first_traded,
|
||||||
self.auto_close_date,
|
self.auto_close_date,
|
||||||
self.exchange_full,
|
self.exchange_full,
|
||||||
self.min_trade_size))
|
self.min_trade_size,
|
||||||
|
self.max_trade_size,
|
||||||
|
self.maker,
|
||||||
|
self.taker,
|
||||||
|
self.lot,
|
||||||
|
self.decimals,
|
||||||
|
self.trading_state,
|
||||||
|
self.data_source))
|
||||||
|
|
||||||
def make_asset_array(int size, Asset asset):
|
def make_asset_array(int size, Asset asset):
|
||||||
cdef np.ndarray out = np.empty([size], dtype=object)
|
cdef np.ndarray out = np.empty([size], dtype=object)
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ from catalyst.utils.numpy_utils import as_column
|
|||||||
from catalyst.utils.preprocess import preprocess
|
from catalyst.utils.preprocess import preprocess
|
||||||
from catalyst.utils.sqlite_utils import group_into_chunks, coerce_string_to_eng
|
from catalyst.utils.sqlite_utils import group_into_chunks, coerce_string_to_eng
|
||||||
|
|
||||||
log = Logger('assets.py')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('assets.py', level=LOG_LEVEL)
|
||||||
|
|
||||||
# A set of fields that need to be converted to strings before building an
|
# A set of fields that need to be converted to strings before building an
|
||||||
# Asset to avoid unicode fields
|
# Asset to avoid unicode fields
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logbook
|
||||||
|
|
||||||
|
''' You can override the LOG level from your environment.
|
||||||
|
For example, if you want to see the DEBUG messages, run:
|
||||||
|
$ export CATALYST_LOG_LEVEL=10
|
||||||
|
'''
|
||||||
|
LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO))
|
||||||
|
|
||||||
|
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
||||||
|
'{exchange}/symbols.json'
|
||||||
|
|
||||||
|
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M'
|
||||||
|
DATE_FORMAT = '%Y-%m-%d'
|
||||||
|
|
||||||
|
try:
|
||||||
|
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
except Exception as e:
|
||||||
|
print('unable to get catalyst path: {}'.format(e))
|
||||||
|
|
||||||
|
AUTO_INGEST = False
|
||||||
|
|
||||||
|
AUTH_SERVER = 'https://data.enigma.co'
|
||||||
|
|
||||||
|
ETH_REMOTE_NODE = 'https://mainnet.infura.io'
|
||||||
|
|
||||||
|
MARKETPLACE_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
|
'catalyst/master/catalyst/marketplace/' \
|
||||||
|
'contract_marketplace_address.txt'
|
||||||
|
|
||||||
|
MARKETPLACE_CONTRACT_ABI = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
|
'catalyst/master/catalyst/marketplace/' \
|
||||||
|
'contract_marketplace_abi.json'
|
||||||
|
|
||||||
|
ENIGMA_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
|
'catalyst/master/catalyst/marketplace/' \
|
||||||
|
'contract_enigma_address.txt'
|
||||||
|
|
||||||
|
ENIGMA_CONTRACT_ABI = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
|
'catalyst/master/catalyst/marketplace/' \
|
||||||
|
'contract_enigma_abi.json'
|
||||||
|
|
||||||
|
SUPPORTED_WALLETS = ['metamask', 'ledger', 'trezor', 'bitbox', 'keystore',
|
||||||
|
'key']
|
||||||
+215
-127
@@ -1,39 +1,47 @@
|
|||||||
import json, time, csv
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import logbook
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import os, time, shutil, requests, logbook
|
import requests
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename
|
|
||||||
|
|
||||||
|
from catalyst.exchange.utils.exchange_utils import \
|
||||||
|
get_exchange_symbols_filename
|
||||||
|
|
||||||
DT_START = int(time.mktime(datetime(2010, 1, 1, 0, 0).timetuple()))
|
DT_START = int(time.mktime(datetime(2010, 1, 1, 0, 0).timetuple()))
|
||||||
DT_END = int(time.time())
|
DT_END = pd.to_datetime('today').value // 10 ** 9
|
||||||
CSV_OUT_FOLDER = '/var/tmp/catalyst/data/poloniex/'
|
CSV_OUT_FOLDER = os.environ.get('CSV_OUT_FOLDER', '/efs/exchanges/poloniex/')
|
||||||
CSV_OUT_FOLDER = '/Volumes/enigma/data/poloniex/'
|
CONN_RETRIES = 2
|
||||||
CONN_RETRIES = 2
|
|
||||||
|
|
||||||
logbook.StderrHandler().push_application()
|
logbook.StderrHandler().push_application()
|
||||||
log = logbook.Logger(__name__)
|
log = logbook.Logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PoloniexCurator(object):
|
class PoloniexCurator(object):
|
||||||
'''
|
'''
|
||||||
OHLCV data feed generator for crypto data. Based on Poloniex market data
|
OHLCV data feed generator for crypto data. Based on Poloniex market data
|
||||||
'''
|
'''
|
||||||
|
|
||||||
_api_path = 'https://poloniex.com/public?'
|
_api_path = 'https://poloniex.com/public?'
|
||||||
currency_pairs = []
|
currency_pairs = []
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if not os.path.exists(CSV_OUT_FOLDER):
|
if not os.path.exists(CSV_OUT_FOLDER):
|
||||||
try:
|
try:
|
||||||
os.makedirs(CSV_OUT_FOLDER)
|
os.makedirs(CSV_OUT_FOLDER)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Failed to create data folder: %s' % CSV_OUT_FOLDER)
|
log.error('Failed to create data folder: {}'.format(
|
||||||
|
CSV_OUT_FOLDER))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
'''
|
|
||||||
Retrieves and returns all currency pairs from the exchange
|
|
||||||
'''
|
|
||||||
def get_currency_pairs(self):
|
def get_currency_pairs(self):
|
||||||
|
'''
|
||||||
|
Retrieves and returns all currency pairs from the exchange
|
||||||
|
'''
|
||||||
url = self._api_path + 'command=returnTicker'
|
url = self._api_path + 'command=returnTicker'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -44,102 +52,154 @@ class PoloniexCurator(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
self.currency_pairs = []
|
self.currency_pairs = []
|
||||||
for ticker in data:
|
for ticker in data:
|
||||||
self.currency_pairs.append(ticker)
|
self.currency_pairs.append(ticker)
|
||||||
self.currency_pairs.sort()
|
self.currency_pairs.sort()
|
||||||
|
|
||||||
log.debug('Currency pairs retrieved successfully: %d' % (len(self.currency_pairs)))
|
log.debug('Currency pairs retrieved successfully: {}'.format(
|
||||||
|
len(self.currency_pairs)
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Helper function that reads tradeID and date fields from CSV readline
|
|
||||||
'''
|
|
||||||
def _retrieve_tradeID_date(self, row):
|
def _retrieve_tradeID_date(self, row):
|
||||||
|
'''
|
||||||
|
Helper function that reads tradeID and date fields from CSV readline
|
||||||
|
'''
|
||||||
tId = int(row.split(',')[0])
|
tId = int(row.split(',')[0])
|
||||||
d = pd.to_datetime( row.split(',')[1], infer_datetime_format=True).value // 10 ** 9
|
d = pd.to_datetime(row.split(',')[1],
|
||||||
|
infer_datetime_format=True).value // 10 ** 9
|
||||||
return tId, d
|
return tId, d
|
||||||
|
|
||||||
'''
|
def retrieve_trade_history(self, currencyPair, start=DT_START,
|
||||||
Retrieves TradeHistory from exchange for a given currencyPair between start and end dates.
|
end=DT_END, temp=None):
|
||||||
If no start date is provided, uses a system-wide one (beginning of time for cryptotrading)
|
'''
|
||||||
If no end date is provided, 'now' is used
|
Retrieves TradeHistory from exchange for a given currencyPair
|
||||||
|
between start and end dates. If no start date is provided, uses
|
||||||
|
a system-wide one (beginning of time for cryptotrading).
|
||||||
|
If no end date is provided, 'now' is used.
|
||||||
|
|
||||||
Stores results in CSV file on disk.
|
Stores results in CSV file on disk.
|
||||||
This function is called recursively to work around the limitations imposed by the provider API.
|
|
||||||
'''
|
This function is called recursively to work around the
|
||||||
def retrieve_trade_history(self, currencyPair, start=DT_START, end=DT_END, temp=None):
|
limitations imposed by the provider API.
|
||||||
|
'''
|
||||||
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Check what data we already have on disk, reading first and last lines from file.
|
Check what data we already have on disk, reading first and last
|
||||||
Data is stored on file from NEWEST to OLDEST.
|
lines from file. Data is stored on file from NEWEST to OLDEST.
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
with open(csv_fn, 'ab+') as f:
|
with open(csv_fn, 'ab+') as f:
|
||||||
f.seek(0, os.SEEK_END)
|
f.seek(0, os.SEEK_END)
|
||||||
if(f.tell() > 2): # First check file is not zero size
|
if(f.tell() > 2): # Check file size is not 0
|
||||||
f.seek(0) # Go to the beginning to read first line
|
f.seek(0) # Go to start to read
|
||||||
last_tradeID, end_file = self._retrieve_tradeID_date(f.readline())
|
last_tradeID, end_file = self._retrieve_tradeID_date(
|
||||||
f.seek(-2, os.SEEK_END) # Jump to the second last byte.
|
f.readline())
|
||||||
while f.read(1) != b"\n": # Until EOL is found...
|
f.seek(-2, os.SEEK_END) # Jump to the 2nd last byte
|
||||||
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
|
while f.read(1) != b"\n": # Until EOL is found...
|
||||||
first_tradeID, start_file = self._retrieve_tradeID_date(f.readline())
|
# ...jump back the read byte plus one more.
|
||||||
|
f.seek(-2, os.SEEK_CUR)
|
||||||
|
first_tradeID, start_file = self._retrieve_tradeID_date(
|
||||||
|
f.readline())
|
||||||
|
|
||||||
if( first_tradeID == 1 and end_file + 3600 > DT_END ):
|
if(end_file + 3600 * 6 > DT_END
|
||||||
|
and (first_tradeID == 1
|
||||||
|
or (currencyPair == 'BTC_HUC'
|
||||||
|
and first_tradeID == 2)
|
||||||
|
or (currencyPair == 'BTC_RIC'
|
||||||
|
and first_tradeID == 2)
|
||||||
|
or (currencyPair == 'BTC_XCP'
|
||||||
|
and first_tradeID == 2)
|
||||||
|
or (currencyPair == 'BTC_NAV'
|
||||||
|
and first_tradeID == 4569)
|
||||||
|
or (currencyPair == 'BTC_POT'
|
||||||
|
and first_tradeID == 23511))):
|
||||||
return
|
return
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Error opening file: %s' % csv_fn)
|
log.error('Error opening file: {}'.format(csv_fn))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Poloniex API limits querying TradeHistory to intervals smaller than 1 month,
|
Poloniex API limits querying TradeHistory to intervals smaller
|
||||||
so we make sure that start date is never more than 1 month apart from end date
|
than 1 month, so we make sure that start date is never more than
|
||||||
|
1 month apart from end date
|
||||||
'''
|
'''
|
||||||
if( end - start > 2419200 ): # 60 s/min * 60 min/hr * 24 hr/day * 28 days
|
if(end - start > 2419200): # 60s/min * 60min/hr * 24hr/day * 28days
|
||||||
newstart = end - 2419200
|
newstart = end - 2419200
|
||||||
else:
|
else:
|
||||||
newstart = start
|
newstart = start
|
||||||
|
|
||||||
log.debug(currencyPair+': Retrieving from '+str(newstart)+' to '+str(end) +'\t '
|
log.debug('{}: Retrieving from {} to {}\t {} - {}'.format(
|
||||||
+ time.ctime(newstart) + ' - '+ time.ctime(end))
|
currencyPair, str(newstart), str(end),
|
||||||
|
time.ctime(newstart), time.ctime(end)))
|
||||||
|
|
||||||
url = self._api_path + 'command=returnTradeHistory¤cyPair=' + currencyPair + '&start=' + str(newstart) + '&end=' + str(end)
|
url = '{path}command=returnTradeHistory¤cyPair={pair}' \
|
||||||
|
'&start={start}&end={end}'.format(
|
||||||
|
path=self._api_path,
|
||||||
|
pair=currencyPair,
|
||||||
|
start=str(newstart),
|
||||||
|
end=str(end)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
attempts = 0
|
||||||
response = requests.get(url)
|
success = 0
|
||||||
except Exception as e:
|
while attempts < CONN_RETRIES:
|
||||||
log.error('Failed to retrieve trade history data for %s' % currencyPair)
|
try:
|
||||||
log.exception(e)
|
response = requests.get(url)
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Failed to retrieve trade history data'
|
||||||
|
'for {}'.format(currencyPair))
|
||||||
|
log.exception(e)
|
||||||
|
attempts += 1
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
if(isinstance(response.json(), dict)
|
||||||
|
and response.json()['error']):
|
||||||
|
log.error('Failed to to retrieve trade history data '
|
||||||
|
'for {}: {}'.format(
|
||||||
|
currencyPair,
|
||||||
|
response.json()['error']
|
||||||
|
))
|
||||||
|
attempts += 1
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
attempts += 1
|
||||||
|
else:
|
||||||
|
success = 1
|
||||||
|
break
|
||||||
|
|
||||||
|
if not success:
|
||||||
return None
|
return None
|
||||||
else:
|
|
||||||
if isinstance(response.json(), dict) and response.json()['error']:
|
|
||||||
log.error('Failed to to retrieve trade history data for %s: %s' % (currencyPair,response.json()['error']))
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
If we get to transactionId == 1, and we already have that on disk,
|
If we get to transactionId == 1, and we already have that on
|
||||||
we got to the end of TradeHistory for this coin.
|
disk, we got to the end of TradeHistory for this coin.
|
||||||
'''
|
'''
|
||||||
if('first_tradeID' in locals() and response.json()[-1]['tradeID'] == first_tradeID):
|
if('first_tradeID' in locals()
|
||||||
|
and response.json()[-1]['tradeID'] == first_tradeID):
|
||||||
return
|
return
|
||||||
|
|
||||||
'''
|
'''
|
||||||
There are primarily two scenarios:
|
There are primarily two scenarios:
|
||||||
a) There is newer data available that we need to add at the beginning
|
a) There is newer data available that we need to add at
|
||||||
of the file. We'll retrieve all what we need until we get to what
|
the beginning of the file. We'll retrieve all what we
|
||||||
we already have, writing it to a temporary file; and we will write
|
need until we get to what we already have, writing it
|
||||||
that at the beginning of our existing file.
|
to a temporary file; and we will write that at the
|
||||||
b) We are going back in time, appending at the end of our existing
|
beginning of our existing file.
|
||||||
TradeHistory until the first transaction for this currencyPair
|
b) We are going back in time, appending at the end of
|
||||||
|
our existing TradeHistory until the first transaction
|
||||||
|
for this currencyPair
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
if( 'end_file' in locals() and end_file + 3600 < end):
|
if(temp is not None
|
||||||
|
or ('end_file' in locals() and end_file + 3600 < end)):
|
||||||
if (temp is None):
|
if (temp is None):
|
||||||
temp = os.tmpfile()
|
temp = os.tmpfile()
|
||||||
tempcsv = csv.writer(temp)
|
tempcsv = csv.writer(temp)
|
||||||
for item in response.json():
|
for item in response.json():
|
||||||
if( item['tradeID'] <= last_tradeID ):
|
if(item['tradeID'] <= last_tradeID):
|
||||||
continue
|
continue
|
||||||
tempcsv.writerow([
|
tempcsv.writerow([
|
||||||
item['tradeID'],
|
item['tradeID'],
|
||||||
@@ -148,24 +208,28 @@ class PoloniexCurator(object):
|
|||||||
item['rate'],
|
item['rate'],
|
||||||
item['amount'],
|
item['amount'],
|
||||||
item['total'],
|
item['total'],
|
||||||
item['globalTradeID']
|
item['globalTradeID'],
|
||||||
])
|
])
|
||||||
if( response.json()[-1]['tradeID'] > last_tradeID ):
|
if(response.json()[-1]['tradeID'] > last_tradeID):
|
||||||
end = pd.to_datetime( response.json()[-1]['date'], infer_datetime_format=True).value // 10 ** 9
|
end = pd.to_datetime(response.json()[-1]['date'],
|
||||||
self.retrieve_trade_history(currencyPair, start, end, temp=temp)
|
infer_datetime_format=True
|
||||||
|
).value // 10**9
|
||||||
|
self.retrieve_trade_history(currencyPair, start,
|
||||||
|
end, temp=temp)
|
||||||
else:
|
else:
|
||||||
with open(csv_fn,'rb+') as f:
|
with open(csv_fn, 'rb+') as f:
|
||||||
shutil.copyfileobj(f,temp)
|
shutil.copyfileobj(f, temp)
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
temp.seek(0)
|
temp.seek(0)
|
||||||
shutil.copyfileobj(temp,f)
|
shutil.copyfileobj(temp, f)
|
||||||
temp.close()
|
temp.close()
|
||||||
end = start_file
|
end = start_file
|
||||||
else:
|
else:
|
||||||
with open(csv_fn, 'ab') as csvfile:
|
with open(csv_fn, 'ab') as csvfile:
|
||||||
csvwriter = csv.writer(csvfile)
|
csvwriter = csv.writer(csvfile)
|
||||||
for item in response.json():
|
for item in response.json():
|
||||||
if( 'first_tradeID' in locals() and item['tradeID'] >= first_tradeID ):
|
if('first_tradeID' in locals()
|
||||||
|
and item['tradeID'] >= first_tradeID):
|
||||||
continue
|
continue
|
||||||
csvwriter.writerow([
|
csvwriter.writerow([
|
||||||
item['tradeID'],
|
item['tradeID'],
|
||||||
@@ -176,52 +240,67 @@ class PoloniexCurator(object):
|
|||||||
item['total'],
|
item['total'],
|
||||||
item['globalTradeID']
|
item['globalTradeID']
|
||||||
])
|
])
|
||||||
end = pd.to_datetime( response.json()[-1]['date'], infer_datetime_format=True).value // 10 ** 9
|
end = pd.to_datetime(response.json()[-1]['date'],
|
||||||
|
infer_datetime_format=True).value//10**9
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Error opening %s' % csv_fn)
|
log.error('Error opening {}'.format(csv_fn))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
If we got here, we aren't done yet. Call recursively with 'end' times
|
If we got here, we aren't done yet. Call recursively with
|
||||||
that go sequentially back in time.
|
'end' times that go sequentially back in time.
|
||||||
'''
|
'''
|
||||||
self.retrieve_trade_history(currencyPair, start, end)
|
self.retrieve_trade_history(currencyPair, start, end)
|
||||||
|
|
||||||
|
def generate_ohlcv(self, df):
|
||||||
'''
|
'''
|
||||||
Generates OHLCV dataframe from a dataframe containing all TradeHistory
|
Generates OHLCV dataframe from a dataframe containing all TradeHistory
|
||||||
by resampling with 1-minute period
|
by resampling with 1-minute period
|
||||||
'''
|
'''
|
||||||
def generate_ohlcv(self, df):
|
df.set_index('date', inplace=True) # Index by date
|
||||||
df.set_index('date', inplace=True) # Index by date
|
vol = df['total'].to_frame('volume') # set Vol aside
|
||||||
vol = df['total'].to_frame('volume') # Will deal with vol separately, as ohlc() messes it up
|
df.drop('total', axis=1, inplace=True) # Drop volume data
|
||||||
df.drop('total', axis=1, inplace=True) # Drop volume data from dataframe
|
ohlc = df.resample('T').ohlc() # Resample OHLC 1min
|
||||||
ohlc = df.resample('T').ohlc() # Resample OHLC in 1min bins
|
ohlc.columns = ohlc.columns.map(lambda t: t[1]) # Rename cols
|
||||||
ohlc.columns = ohlc.columns.map(lambda t: t[1]) # Raname columns by dropping 'rate'
|
closes = ohlc['close'].fillna(method='pad') # Pad fwd missing close
|
||||||
closes = ohlc['close'].fillna(method='pad') # Pad forward missing 'close'
|
ohlc = ohlc.apply(lambda x: x.fillna(closes)) # Fill NA w/ last close
|
||||||
ohlc = ohlc.apply(lambda x: x.fillna(closes)) # Fill N/A with last close
|
vol = vol.resample('T').sum().fillna(0) # Add volumes by bin
|
||||||
vol = vol.resample('T').sum().fillna(0) # Add volumes by bin
|
ohlcv = pd.concat([ohlc, vol], axis=1) # Concat OHLC + Vol
|
||||||
ohlcv = pd.concat([ohlc,vol], axis=1) # Concatenate OHLC + Volume
|
|
||||||
return ohlcv
|
return ohlcv
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Generates OHLCV data file with 1minute bars from TradeHistory on disk
|
|
||||||
'''
|
|
||||||
def write_ohlcv_file(self, currencyPair):
|
def write_ohlcv_file(self, currencyPair):
|
||||||
|
'''
|
||||||
|
Generates OHLCV data file with 1minute bars from TradeHistory on disk
|
||||||
|
'''
|
||||||
csv_trades = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
csv_trades = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
||||||
csv_1min = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
|
csv_1min = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
|
||||||
if( os.path.isfile(csv_1min) ):
|
if(os.path.getmtime(csv_1min) > time.time() - 7200):
|
||||||
log.debug(currencyPair+': 1min data already present. Delete the file if you want to rebuild it.')
|
log.debug(currencyPair+': 1min data file already up to date. '
|
||||||
|
'Delete the file if you want to rebuild it.')
|
||||||
else:
|
else:
|
||||||
df = pd.read_csv(csv_trades, names=['tradeID','date','type','rate','amount','total','globalTradeID'],
|
df = pd.read_csv(csv_trades,
|
||||||
dtype = {'tradeID': int, 'date': str, 'type': str, 'rate': float, 'amount': float, 'total': float, 'globalTradeID': int } )
|
names=['tradeID',
|
||||||
df.drop(['tradeID','type','amount','globalTradeID'], axis=1, inplace=True)
|
'date',
|
||||||
|
'type',
|
||||||
|
'rate',
|
||||||
|
'amount',
|
||||||
|
'total',
|
||||||
|
'globalTradeID'],
|
||||||
|
dtype={'tradeID': int,
|
||||||
|
'date': str,
|
||||||
|
'type': str,
|
||||||
|
'rate': float,
|
||||||
|
'amount': float,
|
||||||
|
'total': float,
|
||||||
|
'globalTradeID': int}
|
||||||
|
)
|
||||||
|
df.drop(['tradeID', 'type', 'amount', 'globalTradeID'],
|
||||||
|
axis=1, inplace=True)
|
||||||
df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True)
|
df['date'] = pd.to_datetime(df['date'], infer_datetime_format=True)
|
||||||
ohlcv = self.generate_ohlcv(df)
|
ohlcv = self.generate_ohlcv(df)
|
||||||
try:
|
try:
|
||||||
with open(csv_1min, 'ab') as csvfile:
|
with open(csv_1min, 'w') as csvfile:
|
||||||
csvwriter = csv.writer(csvfile)
|
csvwriter = csv.writer(csvfile)
|
||||||
for item in ohlcv.itertuples():
|
for item in ohlcv.itertuples():
|
||||||
if item.Index == 0:
|
if item.Index == 0:
|
||||||
@@ -235,25 +314,30 @@ class PoloniexCurator(object):
|
|||||||
item.volume,
|
item.volume,
|
||||||
])
|
])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Error opening %s' % csv_fn)
|
log.error('Error opening {}'.format(csv_1min))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
log.debug(currencyPair+': Generated 1min OHLCV data.')
|
log.debug('{}: Generated 1min OHLCV data.'.format(currencyPair))
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
Returns a data frame for a given currencyPair from data on disk
|
|
||||||
'''
|
|
||||||
def onemin_to_dataframe(self, currencyPair, start, end):
|
def onemin_to_dataframe(self, currencyPair, start, end):
|
||||||
csv_fn = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
|
'''
|
||||||
df = pd.read_csv(csv_fn, names=['date', 'open', 'high', 'low', 'close', 'volume'])
|
Returns a data frame for a given currencyPair from data on disk
|
||||||
df['date'] = pd.to_datetime(df['date'],unit='s')
|
'''
|
||||||
|
csv_fn = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
|
||||||
|
df = pd.read_csv(csv_fn, names=['date',
|
||||||
|
'open',
|
||||||
|
'high',
|
||||||
|
'low',
|
||||||
|
'close',
|
||||||
|
'volume'])
|
||||||
|
df['date'] = pd.to_datetime(df['date'], unit='s')
|
||||||
df.set_index('date', inplace=True)
|
df.set_index('date', inplace=True)
|
||||||
return df[start : end]
|
return df[start:end]
|
||||||
|
|
||||||
'''
|
|
||||||
Generates a symbols.json file with corresponding start_date for each currencyPair
|
|
||||||
'''
|
|
||||||
def generate_symbols_json(self, filename=None):
|
def generate_symbols_json(self, filename=None):
|
||||||
|
'''
|
||||||
|
Generates a symbols.json file with corresponding start_date
|
||||||
|
for each currencyPair
|
||||||
|
'''
|
||||||
symbol_map = {}
|
symbol_map = {}
|
||||||
|
|
||||||
if(filename is None):
|
if(filename is None):
|
||||||
@@ -262,33 +346,37 @@ class PoloniexCurator(object):
|
|||||||
with open(filename, 'w') as symbols:
|
with open(filename, 'w') as symbols:
|
||||||
for currencyPair in self.currency_pairs:
|
for currencyPair in self.currency_pairs:
|
||||||
start = None
|
start = None
|
||||||
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
csv_fn = '{}crypto_trades-{}.csv'.format(
|
||||||
|
CSV_OUT_FOLDER,
|
||||||
|
currencyPair)
|
||||||
with open(csv_fn, 'r') as f:
|
with open(csv_fn, 'r') as f:
|
||||||
f.seek(0, os.SEEK_END)
|
f.seek(0, os.SEEK_END)
|
||||||
if(f.tell() > 2): # First check file is not zero size
|
if(f.tell() > 2): # Check file size is not 0
|
||||||
f.seek(-2, os.SEEK_END) # Jump to the second last byte.
|
f.seek(-2, os.SEEK_END) # Jump to 2nd last byte
|
||||||
while f.read(1) != b"\n": # Until EOL is found...
|
while f.read(1) != b"\n": # Until EOL is found...
|
||||||
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
|
# ...jump back the read byte plus one more.
|
||||||
start = pd.to_datetime( f.readline().split(',')[1], infer_datetime_format=True)
|
f.seek(-2, os.SEEK_CUR)
|
||||||
|
start = pd.to_datetime(f.readline().split(',')[1],
|
||||||
|
infer_datetime_format=True)
|
||||||
|
|
||||||
if(start is None):
|
if(start is None):
|
||||||
start = time.gmtime()
|
start = time.gmtime()
|
||||||
base, market = currencyPair.lower().split('_')
|
base, market = currencyPair.lower().split('_')
|
||||||
symbol = '{market}_{base}'.format( market=market, base=base )
|
symbol = '{market}_{base}'.format(market=market, base=base)
|
||||||
symbol_map[currencyPair] = dict(
|
symbol_map[currencyPair] = dict(
|
||||||
symbol = symbol,
|
symbol=symbol,
|
||||||
start_date = start.strftime("%Y-%m-%d")
|
start_date=start.strftime("%Y-%m-%d")
|
||||||
)
|
)
|
||||||
json.dump(symbol_map, symbols, sort_keys=True, indent=2, separators=(',',':'))
|
json.dump(symbol_map, symbols, sort_keys=True, indent=2,
|
||||||
|
separators=(',', ':'))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
pc = PoloniexCurator()
|
pc = PoloniexCurator()
|
||||||
pc.get_currency_pairs()
|
pc.get_currency_pairs()
|
||||||
#pc.generate_symbols_json()
|
# pc.generate_symbols_json()
|
||||||
|
|
||||||
for currencyPair in pc.currency_pairs:
|
for currencyPair in pc.currency_pairs:
|
||||||
pc.retrieve_trade_history(currencyPair)
|
pc.retrieve_trade_history(currencyPair)
|
||||||
|
log.debug('{} up to date.'.format(currencyPair))
|
||||||
pc.write_ohlcv_file(currencyPair)
|
pc.write_ohlcv_file(currencyPair)
|
||||||
|
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ cpdef _read_bcolz_data(ctable_t table,
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if column_name in ['open', 'high', 'low', 'close']:
|
if column_name in ['open', 'high', 'low', 'close', 'volume']:
|
||||||
where_nan = (outbuf == 0)
|
where_nan = (outbuf == 0)
|
||||||
outbuf_as_float = outbuf.astype(float64) * .000000001
|
outbuf_as_float = outbuf.astype(float64) * .000000001
|
||||||
outbuf_as_float[where_nan] = NAN
|
outbuf_as_float[where_nan] = NAN
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# These imports are necessary to force module-scope register calls to happen.
|
# These imports are necessary to force module-scope register calls to happen.
|
||||||
from . import quandl # noqa
|
from . import quandl # noqa
|
||||||
from . import poloniex
|
|
||||||
from .core import (
|
from .core import (
|
||||||
UnknownBundle,
|
UnknownBundle,
|
||||||
bundles,
|
bundles,
|
||||||
|
|||||||
@@ -13,10 +13,9 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
from itertools import count
|
from itertools import count
|
||||||
import tarfile
|
import tarfile
|
||||||
from time import time, sleep
|
from time import sleep
|
||||||
|
|
||||||
from abc import abstractmethod, abstractproperty
|
from abc import abstractmethod, abstractproperty
|
||||||
import logbook
|
import logbook
|
||||||
@@ -30,11 +29,14 @@ from catalyst.utils.cli import (
|
|||||||
)
|
)
|
||||||
from catalyst.utils.memoize import lazyval
|
from catalyst.utils.memoize import lazyval
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
logbook.StderrHandler().push_application()
|
logbook.StderrHandler().push_application()
|
||||||
log = logbook.Logger(__name__)
|
log = logbook.Logger(__name__, level=LOG_LEVEL)
|
||||||
|
|
||||||
DEFAULT_RETRIES = 5
|
DEFAULT_RETRIES = 5
|
||||||
|
|
||||||
|
|
||||||
class BaseBundle(object):
|
class BaseBundle(object):
|
||||||
def __init__(self, asset_filter=[]):
|
def __init__(self, asset_filter=[]):
|
||||||
self._asset_filter = asset_filter
|
self._asset_filter = asset_filter
|
||||||
@@ -126,7 +128,7 @@ class BaseBundle(object):
|
|||||||
retries = environ.get('CATALYST_DOWNLOAD_ATTEMPTS', 5)
|
retries = environ.get('CATALYST_DOWNLOAD_ATTEMPTS', 5)
|
||||||
|
|
||||||
if is_compile:
|
if is_compile:
|
||||||
# User has instructed local compilation and ingestion of bundle.
|
# User has instructed local compilation & ingestion of bundle.
|
||||||
# Fetch raw metadata for all symbols.
|
# Fetch raw metadata for all symbols.
|
||||||
raw_metadata = self._fetch_metadata_frame(
|
raw_metadata = self._fetch_metadata_frame(
|
||||||
api_key,
|
api_key,
|
||||||
@@ -155,9 +157,9 @@ class BaseBundle(object):
|
|||||||
show_progress=show_progress,
|
show_progress=show_progress,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Post-process metadata using cached symbol frames, and write to
|
# Post-process metadata using cached symbol frames, and write
|
||||||
# disk. This metadata must be written before any attempt to write
|
# to disk. This metadata must be written before any attempt
|
||||||
# minute data.
|
# to write minute data.
|
||||||
metadata = self._post_process_metadata(
|
metadata = self._post_process_metadata(
|
||||||
raw_metadata,
|
raw_metadata,
|
||||||
cache,
|
cache,
|
||||||
@@ -182,10 +184,11 @@ class BaseBundle(object):
|
|||||||
show_progress=show_progress,
|
show_progress=show_progress,
|
||||||
)
|
)
|
||||||
|
|
||||||
# For legacy purposes, this call is required to ensure the database
|
# For legacy purposes, this call is required to ensure the
|
||||||
# contains an appropriately initialized file structure. We don't
|
# database contains an appropriately initialized file
|
||||||
# forsee a usecase for adjustments at this time, but may later
|
# structure. We don't forsee a usecase for adjustments at
|
||||||
# choose to expose this functionality in the future.
|
# this time, but may later choose to expose this functionality
|
||||||
|
# in the future.
|
||||||
adjustment_writer.write(
|
adjustment_writer.write(
|
||||||
splits=(
|
splits=(
|
||||||
pd.concat(self.splits, ignore_index=True)
|
pd.concat(self.splits, ignore_index=True)
|
||||||
@@ -230,11 +233,11 @@ class BaseBundle(object):
|
|||||||
tar.extractall(output_dir)
|
tar.extractall(output_dir)
|
||||||
|
|
||||||
def _fetch_metadata_frame(self,
|
def _fetch_metadata_frame(self,
|
||||||
api_key,
|
api_key,
|
||||||
cache,
|
cache,
|
||||||
retries=DEFAULT_RETRIES,
|
retries=DEFAULT_RETRIES,
|
||||||
environ=None,
|
environ=None,
|
||||||
show_progress=False):
|
show_progress=False):
|
||||||
|
|
||||||
# Setup raw metadata iterator to fetch pages if necessary.
|
# Setup raw metadata iterator to fetch pages if necessary.
|
||||||
raw_iter = self._fetch_metadata_iter(api_key, cache, retries, environ)
|
raw_iter = self._fetch_metadata_iter(api_key, cache, retries, environ)
|
||||||
@@ -267,10 +270,10 @@ class BaseBundle(object):
|
|||||||
page_number,
|
page_number,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
raw = pd.DataFrame([])
|
raw = pd.DataFrame([])
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.exception(
|
log.exception(
|
||||||
'Failed to load metadata from {}. '
|
'Failed to load metadata from {}. '
|
||||||
'Retrying.'.format(self.name)
|
'Retrying.'.format(self.name)
|
||||||
@@ -281,7 +284,6 @@ class BaseBundle(object):
|
|||||||
'attempts.'.format(page_number, retries)
|
'attempts.'.format(page_number, retries)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if raw.empty:
|
if raw.empty:
|
||||||
# Empty DataFrame signals completion.
|
# Empty DataFrame signals completion.
|
||||||
break
|
break
|
||||||
@@ -316,16 +318,16 @@ class BaseBundle(object):
|
|||||||
show_percent=False,
|
show_percent=False,
|
||||||
) as symbols_map:
|
) as symbols_map:
|
||||||
for asset_id, symbol in symbols_map:
|
for asset_id, symbol in symbols_map:
|
||||||
# Attempt to load data from disk, the cache should have an entry
|
# Attempt to load data from disk, the cache should have an
|
||||||
# for each symbol at this point of the execution. If one does
|
# entry for each symbol at this point of the execution. If one
|
||||||
# not exist, we should fail.
|
# does not exist, we should fail.
|
||||||
key = '{sym}.daily.frame'.format(sym=symbol)
|
key = '{sym}.daily.frame'.format(sym=symbol)
|
||||||
try:
|
try:
|
||||||
raw_data = cache[key]
|
raw_data = cache[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Unable to find cached data for symbol: {0}'.format(symbol)
|
'Unable to find cached data for symbol:'
|
||||||
)
|
' {0}'.format(symbol))
|
||||||
|
|
||||||
# Perform and require post-processing of metadata.
|
# Perform and require post-processing of metadata.
|
||||||
final_symbol_metadata = self.post_process_symbol_metadata(
|
final_symbol_metadata = self.post_process_symbol_metadata(
|
||||||
@@ -361,8 +363,8 @@ class BaseBundle(object):
|
|||||||
# returns the cached data unaltered. The `should_sleep` flag
|
# returns the cached data unaltered. The `should_sleep` flag
|
||||||
# indicates that an API call was attempted, and that we should be
|
# indicates that an API call was attempted, and that we should be
|
||||||
# ensure aren't exceeding our rate limit before proceeding to the
|
# ensure aren't exceeding our rate limit before proceeding to the
|
||||||
# next symbol. If the raw_data is updated, it is cached before being
|
# next symbol. If the raw_data is updated, it is cached before
|
||||||
# returned.
|
# being returned.
|
||||||
raw_data, should_sleep = self._maybe_update_symbol_frame(
|
raw_data, should_sleep = self._maybe_update_symbol_frame(
|
||||||
start_time,
|
start_time,
|
||||||
api_key,
|
api_key,
|
||||||
@@ -466,7 +468,6 @@ class BaseBundle(object):
|
|||||||
data_frequency,
|
data_frequency,
|
||||||
)
|
)
|
||||||
raw_data.index = pd.to_datetime(raw_data.index, utc=True)
|
raw_data.index = pd.to_datetime(raw_data.index, utc=True)
|
||||||
#raw_data.index = raw_data.index.tz_localize('UTC')
|
|
||||||
|
|
||||||
# Filter incoming data to fit start and end sessions.
|
# Filter incoming data to fit start and end sessions.
|
||||||
raw_data = raw_data[
|
raw_data = raw_data[
|
||||||
@@ -480,7 +481,7 @@ class BaseBundle(object):
|
|||||||
|
|
||||||
return raw_data
|
return raw_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.exception(
|
log.exception(
|
||||||
'Exception raised fetching {name} data. Retrying.'
|
'Exception raised fetching {name} data. Retrying.'
|
||||||
.format(name=self.name)
|
.format(name=self.name)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
from catalyst.data.bundles.base import BaseBundle
|
from catalyst.data.bundles.base import BaseBundle
|
||||||
from catalyst.utils.memoize import lazyval
|
from catalyst.utils.memoize import lazyval
|
||||||
|
|
||||||
|
|
||||||
class BasePricingBundle(BaseBundle):
|
class BasePricingBundle(BaseBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def md_dtypes(self):
|
def md_dtypes(self):
|
||||||
@@ -38,6 +39,7 @@ class BasePricingBundle(BaseBundle):
|
|||||||
('volume', 'float64'),
|
('volume', 'float64'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class BaseCryptoPricingBundle(BasePricingBundle):
|
class BaseCryptoPricingBundle(BasePricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def calendar_name(self):
|
def calendar_name(self):
|
||||||
@@ -55,6 +57,7 @@ class BaseCryptoPricingBundle(BasePricingBundle):
|
|||||||
def dividends(self):
|
def dividends(self):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
class BaseEquityPricingBundle(BasePricingBundle):
|
class BaseEquityPricingBundle(BasePricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def calendar_name(self):
|
def calendar_name(self):
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from catalyst.utils.cli import maybe_show_progress
|
|||||||
|
|
||||||
ONE_MEGABYTE = 1024 * 1024
|
ONE_MEGABYTE = 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
def asset_db_path(bundle_name, timestr, environ=None, db_version=None):
|
def asset_db_path(bundle_name, timestr, environ=None, db_version=None):
|
||||||
return pth.data_path(
|
return pth.data_path(
|
||||||
asset_db_relative(bundle_name, timestr, environ, db_version),
|
asset_db_relative(bundle_name, timestr, environ, db_version),
|
||||||
@@ -135,6 +136,7 @@ def ingestions_for_bundle(bundle, environ=None):
|
|||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def download_with_progress(url, chunk_size, **progress_kwargs):
|
def download_with_progress(url, chunk_size, **progress_kwargs):
|
||||||
"""
|
"""
|
||||||
Download streaming data from a URL, printing progress information to the
|
Download streaming data from a URL, printing progress information to the
|
||||||
@@ -705,4 +707,5 @@ def _make_bundle_core():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
bundles, register_bundle, register, unregister, ingest, load, clean = _make_bundle_core()
|
bundles, register_bundle, register, unregister, ingest, load, clean = \
|
||||||
|
_make_bundle_core()
|
||||||
|
|||||||
@@ -14,19 +14,17 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
from six.moves.urllib.parse import urlencode
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from six.moves.urllib.parse import urlencode
|
|
||||||
|
|
||||||
from catalyst.data.bundles.core import register_bundle
|
from catalyst.data.bundles.core import register_bundle
|
||||||
from catalyst.data.bundles.base_pricing import BaseCryptoPricingBundle
|
from catalyst.data.bundles.base_pricing import BaseCryptoPricingBundle
|
||||||
from catalyst.utils.memoize import lazyval
|
from catalyst.utils.memoize import lazyval
|
||||||
|
|
||||||
from catalyst.curate.poloniex import PoloniexCurator
|
from catalyst.curate.poloniex import PoloniexCurator
|
||||||
|
|
||||||
|
|
||||||
class PoloniexBundle(BaseCryptoPricingBundle):
|
class PoloniexBundle(BaseCryptoPricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -46,7 +44,8 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
@lazyval
|
@lazyval
|
||||||
def tar_url(self):
|
def tar_url(self):
|
||||||
return (
|
return (
|
||||||
'https://s3.amazonaws.com/enigmaco/catalyst-bundles/poloniex/poloniex-bundle.tar.gz'
|
'https://s3.amazonaws.com/enigmaco/catalyst-bundles/'
|
||||||
|
'poloniex/poloniex-bundle.tar.gz'
|
||||||
)
|
)
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
@@ -67,12 +66,11 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
|
|
||||||
raw = raw.sort_index().reset_index()
|
raw = raw.sort_index().reset_index()
|
||||||
raw.rename(
|
raw.rename(
|
||||||
columns={'index':'symbol'},
|
columns={'index': 'symbol'},
|
||||||
inplace=True,
|
inplace=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
raw = raw[raw['isFrozen'] == 0]
|
raw = raw[raw['isFrozen'] == 0]
|
||||||
|
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
def post_process_symbol_metadata(self, asset_id, sym_md, sym_data):
|
def post_process_symbol_metadata(self, asset_id, sym_md, sym_data):
|
||||||
@@ -98,7 +96,8 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
frequency):
|
frequency):
|
||||||
|
|
||||||
# TODO: replace this with direct exchange call
|
# TODO: replace this with direct exchange call
|
||||||
# The end date and frequency should be used to calculate the number of bars
|
# The end date and frequency should be used to
|
||||||
|
# calculate the number of bars
|
||||||
if(frequency == 'minute'):
|
if(frequency == 'minute'):
|
||||||
pc = PoloniexCurator()
|
pc = PoloniexCurator()
|
||||||
raw = pc.onemin_to_dataframe(symbol, start_date, end_date)
|
raw = pc.onemin_to_dataframe(symbol, start_date, end_date)
|
||||||
@@ -116,8 +115,9 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
)
|
)
|
||||||
raw.set_index('date', inplace=True)
|
raw.set_index('date', inplace=True)
|
||||||
|
|
||||||
# BcolzDailyBarReader introduces a 1/1000 factor in the way pricing is stored
|
# BcolzDailyBarReader introduces a 1/1000 factor in the way
|
||||||
# on disk, which we compensate here to get the right pricing amounts
|
# pricing is stored on disk, which we compensate here to get
|
||||||
|
# the right pricing amounts
|
||||||
# ref: data/us_equity_pricing.py
|
# ref: data/us_equity_pricing.py
|
||||||
scale = 1
|
scale = 1
|
||||||
raw.loc[:, 'open'] /= scale
|
raw.loc[:, 'open'] /= scale
|
||||||
@@ -139,7 +139,6 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
|
|
||||||
return self._format_polo_query(query_params)
|
return self._format_polo_query(query_params)
|
||||||
|
|
||||||
|
|
||||||
def _format_data_url(self,
|
def _format_data_url(self,
|
||||||
api_key,
|
api_key,
|
||||||
symbol,
|
symbol,
|
||||||
@@ -171,6 +170,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
query=urlencode(query_params),
|
query=urlencode(query_params),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
As a second parameter, you can pass an array of currency pairs
|
As a second parameter, you can pass an array of currency pairs
|
||||||
that will be processed as an asset_filter to only process that
|
that will be processed as an asset_filter to only process that
|
||||||
@@ -180,9 +180,7 @@ register_bundle(PoloniexBundle, ['USDT_BTC',])
|
|||||||
For a production environment make sure to use (to bundle all pairs):
|
For a production environment make sure to use (to bundle all pairs):
|
||||||
register_bundle(PoloniexBundle)
|
register_bundle(PoloniexBundle)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
if 'ingest' in sys.argv and '-c' in sys.argv:
|
if 'ingest' in sys.argv and '-c' in sys.argv:
|
||||||
register_bundle(PoloniexBundle)
|
register_bundle(PoloniexBundle)
|
||||||
else:
|
else:
|
||||||
register_bundle(PoloniexBundle, create_writers=False)
|
register_bundle(PoloniexBundle, create_writers=False)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from six.moves.urllib.parse import urlencode
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
from catalyst.data.bundles.core import register_bundle
|
from catalyst.data.bundles.core import register_bundle
|
||||||
@@ -26,23 +25,16 @@ from catalyst.utils.memoize import lazyval
|
|||||||
"""
|
"""
|
||||||
Module for building a complete daily dataset from Quandl's WIKI dataset.
|
Module for building a complete daily dataset from Quandl's WIKI dataset.
|
||||||
"""
|
"""
|
||||||
from itertools import count
|
|
||||||
import tarfile
|
|
||||||
from time import time, sleep
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
import pandas as pd
|
|
||||||
from six.moves.urllib.parse import urlencode
|
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.utils.calendars import register_calendar_alias
|
from catalyst.utils.calendars import register_calendar_alias
|
||||||
from catalyst.utils.cli import maybe_show_progress
|
|
||||||
|
|
||||||
from . import core as bundles
|
|
||||||
|
|
||||||
log = Logger(__name__)
|
log = Logger(__name__, level=LOG_LEVEL)
|
||||||
seconds_per_call = (pd.Timedelta('10 minutes') / 2000).total_seconds()
|
seconds_per_call = (pd.Timedelta('10 minutes') / 2000).total_seconds()
|
||||||
|
|
||||||
|
|
||||||
class QuandlBundle(BaseEquityPricingBundle):
|
class QuandlBundle(BaseEquityPricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -107,8 +99,8 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
# Filter out invalid symbols
|
# Filter out invalid symbols
|
||||||
raw = raw[~raw.symbol.isin(self._excluded_symbols)]
|
raw = raw[~raw.symbol.isin(self._excluded_symbols)]
|
||||||
|
|
||||||
# cut out all the other stuff in the name column
|
# cut out all the other stuff in the name column. We need to
|
||||||
# we need to escape the paren because it is actually splitting on a regex
|
# escape the paren because it is actually splitting on a regex
|
||||||
raw.asset_name = raw.asset_name.str.split(r' \(', 1).str.get(0)
|
raw.asset_name = raw.asset_name.str.split(r' \(', 1).str.get(0)
|
||||||
|
|
||||||
return raw
|
return raw
|
||||||
@@ -173,7 +165,6 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
df['sid'] = asset_id
|
df['sid'] = asset_id
|
||||||
self.splits.append(df)
|
self.splits.append(df)
|
||||||
|
|
||||||
|
|
||||||
def _update_dividends(self, asset_id, raw_data):
|
def _update_dividends(self, asset_id, raw_data):
|
||||||
divs = raw_data.ex_dividend
|
divs = raw_data.ex_dividend
|
||||||
df = pd.DataFrame({'amount': divs[divs != 0]})
|
df = pd.DataFrame({'amount': divs[divs != 0]})
|
||||||
@@ -184,7 +175,6 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
df['record_date'] = df['declared_date'] = df['pay_date'] = pd.NaT
|
df['record_date'] = df['declared_date'] = df['pay_date'] = pd.NaT
|
||||||
self.dividends.append(df)
|
self.dividends.append(df)
|
||||||
|
|
||||||
|
|
||||||
def _format_metadata_url(self, api_key, page_number):
|
def _format_metadata_url(self, api_key, page_number):
|
||||||
"""Build the query RL for the quandl WIKI metadata.
|
"""Build the query RL for the quandl WIKI metadata.
|
||||||
"""
|
"""
|
||||||
@@ -198,10 +188,10 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
query_params = [('api_key', api_key)] + query_params
|
query_params = [('api_key', api_key)] + query_params
|
||||||
|
|
||||||
return (
|
return (
|
||||||
'https://www.quandl.com/api/v3/datasets.csv?' + urlencode(query_params)
|
'https://www.quandl.com/api/v3/datasets.csv?'
|
||||||
|
+ urlencode(query_params)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _format_wiki_url(self,
|
def _format_wiki_url(self,
|
||||||
api_key,
|
api_key,
|
||||||
symbol,
|
symbol,
|
||||||
@@ -227,5 +217,6 @@ class QuandlBundle(BaseEquityPricingBundle):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
register_calendar_alias('QUANDL', 'NYSE')
|
register_calendar_alias('QUANDL', 'NYSE')
|
||||||
register_bundle(QuandlBundle)
|
register_bundle(QuandlBundle)
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import numpy as np
|
|||||||
from numpy import float64, int64, nan
|
from numpy import float64, int64, nan
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pandas import isnull
|
from pandas import isnull
|
||||||
from pandas.tslib import normalize_date
|
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
from six.moves import reduce
|
from six.moves import reduce
|
||||||
|
|
||||||
@@ -68,7 +67,9 @@ from catalyst.errors import (
|
|||||||
HistoryWindowStartsBeforeData,
|
HistoryWindowStartsBeforeData,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = Logger('DataPortal')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('DataPortal', level=LOG_LEVEL)
|
||||||
|
|
||||||
BASE_FIELDS = frozenset([
|
BASE_FIELDS = frozenset([
|
||||||
"open",
|
"open",
|
||||||
@@ -437,7 +438,7 @@ class DataPortal(object):
|
|||||||
(isinstance(asset, (Asset, ContinuousFuture))))
|
(isinstance(asset, (Asset, ContinuousFuture))))
|
||||||
|
|
||||||
def _get_fetcher_value(self, asset, field, dt):
|
def _get_fetcher_value(self, asset, field, dt):
|
||||||
day = normalize_date(dt)
|
day = dt.normalize()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return \
|
return \
|
||||||
@@ -654,11 +655,11 @@ class DataPortal(object):
|
|||||||
return spot_value
|
return spot_value
|
||||||
|
|
||||||
def _get_minutely_spot_value(self,
|
def _get_minutely_spot_value(self,
|
||||||
asset,
|
asset,
|
||||||
column,
|
column,
|
||||||
dt,
|
dt,
|
||||||
data_frequency,
|
data_frequency,
|
||||||
ffill=False):
|
ffill=False):
|
||||||
|
|
||||||
reader = self._get_pricing_reader(data_frequency)
|
reader = self._get_pricing_reader(data_frequency)
|
||||||
|
|
||||||
@@ -1128,7 +1129,7 @@ class DataPortal(object):
|
|||||||
if self._asset_start_dates[sid] > dt:
|
if self._asset_start_dates[sid] > dt:
|
||||||
raise NoTradeDataAvailableTooEarly(
|
raise NoTradeDataAvailableTooEarly(
|
||||||
sid=sid,
|
sid=sid,
|
||||||
dt=normalize_date(dt),
|
dt=dt.normalize(),
|
||||||
start_dt=start_date
|
start_dt=start_date
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1136,7 +1137,7 @@ class DataPortal(object):
|
|||||||
if self._asset_end_dates[sid] < dt:
|
if self._asset_end_dates[sid] < dt:
|
||||||
raise NoTradeDataAvailableTooLate(
|
raise NoTradeDataAvailableTooLate(
|
||||||
sid=sid,
|
sid=sid,
|
||||||
dt=normalize_date(dt),
|
dt=dt.normalize(),
|
||||||
end_dt=end_date
|
end_dt=end_date
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1260,7 +1261,7 @@ class DataPortal(object):
|
|||||||
if self._extra_source_df is None:
|
if self._extra_source_df is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
day = normalize_date(dt)
|
day = dt.normalize()
|
||||||
|
|
||||||
if day in self._extra_source_df.index:
|
if day in self._extra_source_df.index:
|
||||||
assets = self._extra_source_df.loc[day]['sid']
|
assets = self._extra_source_df.loc[day]['sid']
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ class AssetDispatchBarReader(with_metaclass(ABCMeta)):
|
|||||||
if self._last_available_dt is not None:
|
if self._last_available_dt is not None:
|
||||||
return self._last_available_dt
|
return self._last_available_dt
|
||||||
else:
|
else:
|
||||||
return min(r.last_available_dt for r in self._readers.values())
|
return min(r.last_available_dt for r in list(self._readers.values()))
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
def first_trading_day(self):
|
def first_trading_day(self):
|
||||||
return max(r.first_trading_day for r in self._readers.values())
|
return max(r.first_trading_day for r in list(self._readers.values()))
|
||||||
|
|
||||||
def get_value(self, sid, dt, field):
|
def get_value(self, sid, dt, field):
|
||||||
asset = self._asset_finder.retrieve_asset(sid)
|
asset = self._asset_finder.retrieve_asset(sid)
|
||||||
@@ -133,11 +133,13 @@ class AssetDispatchBarReader(with_metaclass(ABCMeta)):
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
class AssetDispatchMinuteBarReader(AssetDispatchBarReader):
|
class AssetDispatchMinuteBarReader(AssetDispatchBarReader):
|
||||||
|
|
||||||
def _dt_window_size(self, start_dt, end_dt):
|
def _dt_window_size(self, start_dt, end_dt):
|
||||||
return len(self.trading_calendar.minutes_in_range(start_dt, end_dt))
|
return len(self.trading_calendar.minutes_in_range(start_dt, end_dt))
|
||||||
|
|
||||||
|
|
||||||
class AssetDispatchSessionBarReader(AssetDispatchBarReader):
|
class AssetDispatchSessionBarReader(AssetDispatchBarReader):
|
||||||
|
|
||||||
def _dt_window_size(self, start_dt, end_dt):
|
def _dt_window_size(self, start_dt, end_dt):
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from abc import (
|
|||||||
from numpy import concatenate
|
from numpy import concatenate
|
||||||
from lru import LRU
|
from lru import LRU
|
||||||
from pandas import isnull
|
from pandas import isnull
|
||||||
from pandas.tslib import normalize_date
|
|
||||||
from toolz import sliding_window
|
from toolz import sliding_window
|
||||||
|
|
||||||
from six import with_metaclass
|
from six import with_metaclass
|
||||||
@@ -93,8 +92,8 @@ class HistoryCompatibleUSEquityAdjustmentReader(object):
|
|||||||
The adjustments as a dict of loc -> Float64Multiply
|
The adjustments as a dict of loc -> Float64Multiply
|
||||||
"""
|
"""
|
||||||
sid = int(asset)
|
sid = int(asset)
|
||||||
start = normalize_date(dts[0])
|
start = dts[0].normalize()
|
||||||
end = normalize_date(dts[-1])
|
end = dts[-1].normalize()
|
||||||
adjs = {}
|
adjs = {}
|
||||||
if field != 'volume':
|
if field != 'volume':
|
||||||
mergers = self._adjustments_reader.get_adjustments_for_sid(
|
mergers = self._adjustments_reader.get_adjustments_for_sid(
|
||||||
|
|||||||
+29
-83
@@ -12,7 +12,6 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import datetime
|
|
||||||
import os
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
@@ -23,6 +22,7 @@ from pandas_datareader.data import DataReader
|
|||||||
from six import iteritems
|
from six import iteritems
|
||||||
from six.moves.urllib_error import HTTPError
|
from six.moves.urllib_error import HTTPError
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.utils.calendars import get_calendar
|
from catalyst.utils.calendars import get_calendar
|
||||||
from . import treasuries, treasuries_can
|
from . import treasuries, treasuries_can
|
||||||
from .benchmarks import get_benchmark_returns
|
from .benchmarks import get_benchmark_returns
|
||||||
@@ -32,7 +32,7 @@ from ..utils.paths import (
|
|||||||
data_root,
|
data_root,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logbook.Logger('Loader')
|
logger = logbook.Logger('Loader', level=LOG_LEVEL)
|
||||||
|
|
||||||
# Mapping from index symbol to appropriate bond data
|
# Mapping from index symbol to appropriate bond data
|
||||||
INDEX_MAPPING = {
|
INDEX_MAPPING = {
|
||||||
@@ -101,7 +101,7 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
trading_day = get_calendar('OPEN').trading_day
|
trading_day = get_calendar('OPEN').trading_day
|
||||||
|
|
||||||
# TODO: consider making configurable
|
# TODO: consider making configurable
|
||||||
bm_symbol = 'btc_usdt'
|
bm_symbol = 'btc_usd'
|
||||||
# if trading_days is None:
|
# if trading_days is None:
|
||||||
# trading_days = get_calendar('OPEN').schedule
|
# trading_days = get_calendar('OPEN').schedule
|
||||||
|
|
||||||
@@ -127,11 +127,13 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
# before this date.
|
# before this date.
|
||||||
'''
|
'''
|
||||||
if(bundle_data):
|
if(bundle_data):
|
||||||
# If we are using the bundle to retrieve the cryptobenchmark, find the last
|
# If we are using the bundle to retrieve the cryptobenchmark, find
|
||||||
# date for which there is trading data in the bundle
|
# the last date for which there is trading data in the bundle
|
||||||
asset = bundle_data.asset_finder.lookup_symbol(symbol=bm_symbol,as_of_date=None)
|
asset = bundle_data.asset_finder.lookup_symbol(
|
||||||
|
symbol=bm_symbol,as_of_date=None)
|
||||||
ix = bundle_data.daily_bar_reader._last_rows[asset.sid]
|
ix = bundle_data.daily_bar_reader._last_rows[asset.sid]
|
||||||
last_date = pd.to_datetime(bundle_data.daily_bar_reader._spot_col('day')[ix],unit='s')
|
last_date = pd.to_datetime(
|
||||||
|
bundle_data.daily_bar_reader._spot_col('day')[ix],unit='s')
|
||||||
else:
|
else:
|
||||||
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
||||||
'''
|
'''
|
||||||
@@ -140,27 +142,31 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
if exchange is None:
|
if exchange is None:
|
||||||
# This is exceptional, since placing the import at the module scope
|
# This is exceptional, since placing the import at the module scope
|
||||||
# breaks things and it's only needed here
|
# breaks things and it's only needed here
|
||||||
from catalyst.exchange.poloniex.poloniex import Poloniex
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
exchange = Poloniex('', '', '')
|
exchange = get_exchange(
|
||||||
|
exchange_name='bitfinex', base_currency='usd'
|
||||||
|
)
|
||||||
|
exchange.init()
|
||||||
|
|
||||||
benchmark_asset = exchange.get_asset(bm_symbol)
|
benchmark_asset = exchange.get_asset(bm_symbol)
|
||||||
|
|
||||||
# exchange.get_history_window() already ensures that we have the right data
|
# exchange.get_history_window() already ensures that we have the right data
|
||||||
# for the right dates
|
# for the right dates
|
||||||
br = exchange.get_history_window(
|
br = exchange.get_history_window_with_bundle(
|
||||||
assets=[benchmark_asset],
|
assets=[benchmark_asset],
|
||||||
end_dt=last_date,
|
end_dt=last_date,
|
||||||
bar_count=pd.Timedelta(last_date - start_dt).days,
|
bar_count=pd.Timedelta(last_date - start_dt).days,
|
||||||
frequency='1d',
|
frequency='1d',
|
||||||
field='close',
|
field='close',
|
||||||
data_frequency='daily')
|
data_frequency='daily',
|
||||||
|
force_auto_ingest=True)
|
||||||
br.columns = ['close']
|
br.columns = ['close']
|
||||||
br = br.pct_change(1).iloc[1:]
|
br = br.pct_change(1).iloc[1:]
|
||||||
br.loc[start_dt] = 0
|
br.loc[start_dt] = 0
|
||||||
br = br.sort_index()
|
br = br.sort_index()
|
||||||
|
|
||||||
# Override first_date for treasury data since we have it for many more years
|
# Override first_date for treasury data since we have it for many more
|
||||||
# and is independent of crypto data
|
# years and is independent of crypto data
|
||||||
first_date_treasury = pd.Timestamp('1990-01-02', tz='UTC')
|
first_date_treasury = pd.Timestamp('1990-01-02', tz='UTC')
|
||||||
tc = ensure_treasury_data(
|
tc = ensure_treasury_data(
|
||||||
bm_symbol,
|
bm_symbol,
|
||||||
@@ -296,14 +302,14 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
|
|
||||||
if (bundle == 'poloniex'):
|
if (bundle == 'poloniex'):
|
||||||
'''
|
'''
|
||||||
If we're using the Poloniex bundle, we'll get the benchmark from the bundle
|
If we're using the Poloniex bundle, we'll get the benchmark from the
|
||||||
instead of downloading it from Poloniex every time we need it.
|
bundle instead of downloading it from Poloniex every time we need it.
|
||||||
Poloniex has a captcha for API queries originating from outside the US that
|
Poloniex has a captcha for API queries originating from outside the US
|
||||||
prevents users abroad from getting Catalyst to work
|
that prevents users abroad from getting Catalyst to work
|
||||||
'''
|
'''
|
||||||
logger.info(
|
logger.info(
|
||||||
(
|
('Retrieving benchmark data from bundle for {symbol!r}'
|
||||||
'Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'),
|
' from {first_date} to {last_date}'),
|
||||||
symbol=symbol, first_date=first_date, last_date=last_date)
|
symbol=symbol, first_date=first_date, last_date=last_date)
|
||||||
|
|
||||||
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,
|
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,
|
||||||
@@ -326,10 +332,11 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# This is how it used to be: downloading the benchmark everytime.
|
# This is how it used to be: downloading the benchmark everytime.
|
||||||
# Leaving this code here to be repurposed in the future for other bundles.
|
# Leaving this code here to be repurposed in the future for
|
||||||
|
# other bundles.
|
||||||
logger.info(
|
logger.info(
|
||||||
(
|
('Downloading benchmark data for {symbol!r}'
|
||||||
'Downloading benchmark data for {symbol!r} from {first_date} to {last_date}'),
|
' from {first_date} to {last_date}'),
|
||||||
symbol=symbol, first_date=first_date, last_date=last_date)
|
symbol=symbol, first_date=first_date, last_date=last_date)
|
||||||
|
|
||||||
raise DeprecationWarning('poloniex bundle deprecated')
|
raise DeprecationWarning('poloniex bundle deprecated')
|
||||||
@@ -426,67 +433,6 @@ def ensure_benchmark_data(symbol, first_date, last_date, now, trading_day,
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def ensure_benchmark_data(symbol, first_date, last_date, now, trading_day,
|
|
||||||
environ=None):
|
|
||||||
"""
|
|
||||||
Ensure we have benchmark data for `symbol` from `first_date` to `last_date`
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
symbol : str
|
|
||||||
The symbol for the benchmark to load.
|
|
||||||
first_date : pd.Timestamp
|
|
||||||
First required date for the cache.
|
|
||||||
last_date : pd.Timestamp
|
|
||||||
Last required date for the cache.
|
|
||||||
now : pd.Timestamp
|
|
||||||
The current time. This is used to prevent repeated attempts to
|
|
||||||
re-download data that isn't available due to scheduling quirks or other
|
|
||||||
failures.
|
|
||||||
trading_day : pd.CustomBusinessDay
|
|
||||||
A trading day delta. Used to find the day before first_date so we can
|
|
||||||
get the close of the day prior to first_date.
|
|
||||||
|
|
||||||
We attempt to download data unless we already have data stored at the data
|
|
||||||
cache for `symbol` whose first entry is before or on `first_date` and whose
|
|
||||||
last entry is on or after `last_date`.
|
|
||||||
|
|
||||||
If we perform a download and the cache criteria are not satisfied, we wait
|
|
||||||
at least one hour before attempting a redownload. This is determined by
|
|
||||||
comparing the current time to the result of os.path.getmtime on the cache
|
|
||||||
path.
|
|
||||||
"""
|
|
||||||
filename = get_benchmark_filename(symbol)
|
|
||||||
data = _load_cached_data(filename, first_date, last_date, now, 'benchmark',
|
|
||||||
environ)
|
|
||||||
if data is not None:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# If no cached data was found or it was missing any dates then download the
|
|
||||||
# necessary data.
|
|
||||||
logger.info(
|
|
||||||
('Downloading benchmark data for {symbol!r} '
|
|
||||||
'from {first_date} to {last_date}'),
|
|
||||||
symbol=symbol,
|
|
||||||
first_date=first_date - trading_day,
|
|
||||||
last_date=last_date
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = get_benchmark_returns(
|
|
||||||
symbol,
|
|
||||||
first_date - trading_day,
|
|
||||||
last_date,
|
|
||||||
)
|
|
||||||
data.to_csv(get_data_filepath(filename, environ))
|
|
||||||
except (OSError, IOError, HTTPError):
|
|
||||||
logger.exception('Failed to cache the new benchmark returns')
|
|
||||||
raise
|
|
||||||
if not has_data_for_dates(data, first_date, last_date):
|
|
||||||
logger.warn("Still don't have expected data after redownload!")
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_treasury_data(symbol, first_date, last_date, now, environ=None):
|
def ensure_treasury_data(symbol, first_date, last_date, now, environ=None):
|
||||||
"""
|
"""
|
||||||
Ensure we have treasury data from treasury module associated with
|
Ensure we have treasury data from treasury module associated with
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ from catalyst.utils.calendars import get_calendar
|
|||||||
from catalyst.utils.cli import maybe_show_progress
|
from catalyst.utils.cli import maybe_show_progress
|
||||||
from catalyst.utils.memoize import lazyval
|
from catalyst.utils.memoize import lazyval
|
||||||
|
|
||||||
logger = logbook.Logger('MinuteBars')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
logger = logbook.Logger('MinuteBars', level=LOG_LEVEL)
|
||||||
|
|
||||||
US_EQUITIES_MINUTES_PER_DAY = 390
|
US_EQUITIES_MINUTES_PER_DAY = 390
|
||||||
FUTURES_MINUTES_PER_DAY = 1440
|
FUTURES_MINUTES_PER_DAY = 1440
|
||||||
@@ -339,12 +341,10 @@ class BcolzMinuteBarMetadata(object):
|
|||||||
'end_session': str(self.end_session.date()),
|
'end_session': str(self.end_session.date()),
|
||||||
# Write these values for backwards compatibility
|
# Write these values for backwards compatibility
|
||||||
'first_trading_day': str(self.start_session.date()),
|
'first_trading_day': str(self.start_session.date()),
|
||||||
'market_opens': (
|
'market_opens': (market_opens.values.astype('datetime64[m]').
|
||||||
market_opens.values.astype('datetime64[m]').
|
astype(np.int64).tolist()),
|
||||||
astype(np.int64).tolist()),
|
'market_closes': (market_closes.values.astype('datetime64[m]').
|
||||||
'market_closes': (
|
astype(np.int64).tolist()),
|
||||||
market_closes.values.astype('datetime64[m]').
|
|
||||||
astype(np.int64).tolist()),
|
|
||||||
}
|
}
|
||||||
with open(self.metadata_path(rootdir), 'w+') as fp:
|
with open(self.metadata_path(rootdir), 'w+') as fp:
|
||||||
json.dump(metadata, fp)
|
json.dump(metadata, fp)
|
||||||
@@ -1254,8 +1254,8 @@ class BcolzMinuteBarReader(MinuteBarReader):
|
|||||||
values = carray[start_idx:end_idx + 1]
|
values = carray[start_idx:end_idx + 1]
|
||||||
if indices_to_exclude is not None:
|
if indices_to_exclude is not None:
|
||||||
for excl_start, excl_stop in indices_to_exclude[::-1]:
|
for excl_start, excl_stop in indices_to_exclude[::-1]:
|
||||||
excl_slice = np.s_[
|
excl_slice = np.s_[excl_start - start_idx:excl_stop
|
||||||
excl_start - start_idx:excl_stop - start_idx + 1]
|
- start_idx + 1]
|
||||||
values = np.delete(values, excl_slice)
|
values = np.delete(values, excl_slice)
|
||||||
|
|
||||||
where = values != 0
|
where = values != 0
|
||||||
@@ -1318,9 +1318,8 @@ class H5MinuteBarUpdateWriter(object):
|
|||||||
|
|
||||||
def __init__(self, path, complevel=None, complib=None):
|
def __init__(self, path, complevel=None, complib=None):
|
||||||
self._complevel = complevel if complevel \
|
self._complevel = complevel if complevel \
|
||||||
is not None else self._COMPLEVEL
|
is not None else self._COMPLEVEL
|
||||||
self._complib = complib if complib \
|
self._complib = complib if complib is not None else self._COMPLIB
|
||||||
is not None else self._COMPLIB
|
|
||||||
self._path = path
|
self._path = path
|
||||||
|
|
||||||
def write(self, frames):
|
def write(self, frames):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from __future__ import division # Python2 req to have division of ints yield float
|
from __future__ import division # Python2 req for division of ints yield float
|
||||||
|
|
||||||
from errno import ENOENT
|
from errno import ENOENT
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -49,7 +49,6 @@ from pandas import (
|
|||||||
to_datetime,
|
to_datetime,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
)
|
)
|
||||||
from pandas.tslib import iNaT
|
|
||||||
from six import (
|
from six import (
|
||||||
iteritems,
|
iteritems,
|
||||||
string_types,
|
string_types,
|
||||||
@@ -83,7 +82,9 @@ from catalyst.utils.cli import (
|
|||||||
from ._equities import _compute_row_slices, _read_bcolz_data
|
from ._equities import _compute_row_slices, _read_bcolz_data
|
||||||
from ._adjustments import load_adjustments_from_sqlite
|
from ._adjustments import load_adjustments_from_sqlite
|
||||||
|
|
||||||
logger = logbook.Logger('UsEquityPricing')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
logger = logbook.Logger('UsEquityPricing', level=LOG_LEVEL)
|
||||||
|
|
||||||
OHLC = frozenset(['open', 'high', 'low', 'close'])
|
OHLC = frozenset(['open', 'high', 'low', 'close'])
|
||||||
OHLCV = frozenset(['open', 'high', 'low', 'close', 'volume'])
|
OHLCV = frozenset(['open', 'high', 'low', 'close', 'volume'])
|
||||||
@@ -118,7 +119,8 @@ SQLITE_STOCK_DIVIDEND_PAYOUT_COLUMN_DTYPES = {
|
|||||||
UINT32_MAX = iinfo(uint32).max
|
UINT32_MAX = iinfo(uint32).max
|
||||||
UINT64_MAX = iinfo(uint64).max
|
UINT64_MAX = iinfo(uint64).max
|
||||||
|
|
||||||
PRICE_ADJUSTMENT_FACTOR = 1000000000 # Provides 9 decimals resolution. Also affects _equities.pyx L220
|
# Provides 9 decimals resolution. Also affects _equities.pyx L220
|
||||||
|
PRICE_ADJUSTMENT_FACTOR = 1000000000
|
||||||
|
|
||||||
|
|
||||||
def check_uint32_safe(value, colname):
|
def check_uint32_safe(value, colname):
|
||||||
@@ -128,6 +130,7 @@ def check_uint32_safe(value, colname):
|
|||||||
"for uint32" % (value, colname)
|
"for uint32" % (value, colname)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def check_uint64_safe(value, colname):
|
def check_uint64_safe(value, colname):
|
||||||
if value >= UINT64_MAX:
|
if value >= UINT64_MAX:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -320,8 +323,8 @@ class BcolzDailyBarWriter(object):
|
|||||||
# Maps column name -> output carray.
|
# Maps column name -> output carray.
|
||||||
columns = {
|
columns = {
|
||||||
k: carray(array([], dtype=uint64))
|
k: carray(array([], dtype=uint64))
|
||||||
if k in OHLCV
|
if k in OHLCV
|
||||||
else carray(array([], dtype=uint32))
|
else carray(array([], dtype=uint32))
|
||||||
for k in US_EQUITY_PRICING_BCOLZ_COLUMNS
|
for k in US_EQUITY_PRICING_BCOLZ_COLUMNS
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +421,7 @@ class BcolzDailyBarWriter(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
full_table.attrs['first_trading_day'] = (
|
full_table.attrs['first_trading_day'] = (
|
||||||
earliest_date if earliest_date is not None else iNaT
|
earliest_date if earliest_date is not None else NaT
|
||||||
)
|
)
|
||||||
|
|
||||||
full_table.attrs['first_row'] = first_row
|
full_table.attrs['first_row'] = first_row
|
||||||
@@ -437,11 +440,13 @@ class BcolzDailyBarWriter(object):
|
|||||||
return raw_data
|
return raw_data
|
||||||
|
|
||||||
winsorise_uint64(raw_data, invalid_data_behavior, 'volume', *OHLC)
|
winsorise_uint64(raw_data, invalid_data_behavior, 'volume', *OHLC)
|
||||||
processed = (raw_data[list(OHLC)] * PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
processed = (raw_data[list(OHLC)]
|
||||||
|
* PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
||||||
dates = raw_data.index.values.astype('datetime64[s]')
|
dates = raw_data.index.values.astype('datetime64[s]')
|
||||||
check_uint32_safe(dates.max().view(np.int64), 'day')
|
check_uint32_safe(dates.max().view(np.int64), 'day')
|
||||||
processed['day'] = dates.astype('uint32')
|
processed['day'] = dates.astype('uint32')
|
||||||
processed['volume'] = (raw_data.volume * PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
processed['volume'] = (raw_data.volume
|
||||||
|
* PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
||||||
return ctable.fromdataframe(processed)
|
return ctable.fromdataframe(processed)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
An overview of most of the trading strategies in this folder can be found in the
|
||||||
|
`Examples Algorithms <https://enigmampc.github.io/catalyst/example-algos.html>`_
|
||||||
|
section of our documentation website.
|
||||||
@@ -6,7 +6,7 @@ from catalyst.api import (
|
|||||||
symbol,
|
symbol,
|
||||||
get_open_orders
|
get_open_orders
|
||||||
)
|
)
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
algo_namespace = 'arbitrage_eth_btc'
|
algo_namespace = 'arbitrage_eth_btc'
|
||||||
@@ -83,15 +83,15 @@ def place_orders(context, amount, buying_price, selling_price, action):
|
|||||||
else:
|
else:
|
||||||
raise ValueError('invalid order action')
|
raise ValueError('invalid order action')
|
||||||
|
|
||||||
base_currency = enter_exchange.base_currency
|
quote_currency = enter_exchange.quote_currency
|
||||||
base_currency_amount = enter_exchange.portfolio.cash
|
quote_currency_amount = enter_exchange.portfolio.cash
|
||||||
|
|
||||||
exit_balances = exit_exchange.get_balances()
|
exit_balances = exit_exchange.get_balances()
|
||||||
exit_currency = context.trading_pairs[
|
exit_currency = context.trading_pairs[
|
||||||
context.selling_exchange].market_currency
|
context.selling_exchange].quote_currency
|
||||||
|
|
||||||
if exit_currency in exit_balances:
|
if exit_currency in exit_balances:
|
||||||
market_currency_amount = exit_balances[exit_currency]
|
quote_currency_amount = exit_balances[exit_currency]
|
||||||
else:
|
else:
|
||||||
log.warn(
|
log.warn(
|
||||||
'the selling exchange {exchange_name} does not hold '
|
'the selling exchange {exchange_name} does not hold '
|
||||||
@@ -102,25 +102,25 @@ def place_orders(context, amount, buying_price, selling_price, action):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if base_currency_amount < (amount * entry_price):
|
if quote_currency_amount < (amount * entry_price):
|
||||||
adj_amount = base_currency_amount / entry_price
|
adj_amount = quote_currency_amount / entry_price
|
||||||
log.warn(
|
log.warn(
|
||||||
'not enough {base_currency} ({base_currency_amount}) to buy '
|
'not enough {quote_currency} ({quote_currency_amount}) to buy '
|
||||||
'{amount}, adjusting the amount to {adj_amount}'.format(
|
'{amount}, adjusting the amount to {adj_amount}'.format(
|
||||||
base_currency=base_currency,
|
quote_currency=quote_currency,
|
||||||
base_currency_amount=base_currency_amount,
|
quote_currency_amount=quote_currency_amount,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
adj_amount=adj_amount
|
adj_amount=adj_amount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
amount = adj_amount
|
amount = adj_amount
|
||||||
|
|
||||||
elif market_currency_amount < amount:
|
elif quote_currency_amount < amount:
|
||||||
log.warn(
|
log.warn(
|
||||||
'not enough {currency} ({currency_amount}) to sell '
|
'not enough {currency} ({currency_amount}) to sell '
|
||||||
'{amount}, aborting'.format(
|
'{amount}, aborting'.format(
|
||||||
currency=exit_currency,
|
currency=exit_currency,
|
||||||
currency_amount=market_currency_amount,
|
currency_amount=quote_currency_amount,
|
||||||
amount=amount
|
amount=amount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -263,13 +263,20 @@ def analyze(context, stats):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
if __name__ == '__main__':
|
||||||
initialize=initialize,
|
# The execution mode: backtest or live
|
||||||
handle_data=handle_data,
|
MODE = 'live'
|
||||||
analyze=analyze,
|
if MODE == 'live':
|
||||||
exchange_name='poloniex,bitfinex',
|
run_algorithm(
|
||||||
live=True,
|
capital_base=0.1,
|
||||||
algo_namespace=algo_namespace,
|
initialize=initialize,
|
||||||
base_currency='btc',
|
handle_data=handle_data,
|
||||||
live_graph=False
|
analyze=analyze,
|
||||||
)
|
exchange_name='poloniex,bitfinex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=algo_namespace,
|
||||||
|
base_currency='btc',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=True,
|
||||||
|
stats_output=None,
|
||||||
|
)
|
||||||
|
|||||||
@@ -14,30 +14,25 @@
|
|||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import (order_target_value, symbol, record,
|
||||||
|
cancel_order, get_open_orders, )
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
order_target_value,
|
|
||||||
symbol,
|
|
||||||
record,
|
|
||||||
cancel_order,
|
|
||||||
get_open_orders,
|
|
||||||
)
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.ASSET_NAME = 'USDT_BTC'
|
context.ASSET_NAME = 'btc_usdt'
|
||||||
context.TARGET_HODL_RATIO = 0.8
|
context.TARGET_HODL_RATIO = 0.8
|
||||||
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
||||||
|
|
||||||
# For all trading pairs in the poloniex bundle, the default denomination
|
|
||||||
# currently supported by Catalyst is 1/1000th of a full coin. Use this
|
|
||||||
# constant to scale the price of up to that of a full coin if desired.
|
|
||||||
context.TICK_SIZE = 1000.0
|
|
||||||
|
|
||||||
context.is_buying = True
|
context.is_buying = True
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
context.i = 0
|
context.i = 0
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
context.i += 1
|
context.i += 1
|
||||||
|
|
||||||
@@ -56,48 +51,49 @@ def handle_data(context, data):
|
|||||||
context.is_buying = False
|
context.is_buying = False
|
||||||
|
|
||||||
# Retrieve current asset price from pricing data
|
# Retrieve current asset price from pricing data
|
||||||
price = data[context.asset].price
|
price = data.current(context.asset, 'price')
|
||||||
|
|
||||||
# Check if still buying and could (approximately) afford another purchase
|
# Check if still buying and could (approximately) afford another purchase
|
||||||
if context.is_buying and cash > price:
|
if context.is_buying and cash > price:
|
||||||
|
print('buying')
|
||||||
# Place order to make position in asset equal to target_hodl_value
|
# Place order to make position in asset equal to target_hodl_value
|
||||||
order_target_value(
|
order_target_value(
|
||||||
context.asset,
|
context.asset,
|
||||||
target_hodl_value,
|
target_hodl_value,
|
||||||
limit_price=price*1.1,
|
limit_price=price * 1.1,
|
||||||
stop_price=price*0.9,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
record(
|
record(
|
||||||
price=price,
|
price=price,
|
||||||
volume=data[context.asset].volume,
|
volume=data.current(context.asset, 'volume'),
|
||||||
cash=cash,
|
cash=cash,
|
||||||
starting_cash=context.portfolio.starting_cash,
|
starting_cash=context.portfolio.starting_cash,
|
||||||
leverage=context.account.leverage,
|
leverage=context.account.leverage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def analyze(context=None, results=None):
|
def analyze(context=None, results=None):
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
# Plot the portfolio and asset data.
|
# Plot the portfolio and asset data.
|
||||||
ax1 = plt.subplot(611)
|
ax1 = plt.subplot(611)
|
||||||
results[['portfolio_value']].plot(ax=ax1)
|
results[['portfolio_value']].plot(ax=ax1)
|
||||||
ax1.set_ylabel('Portfolio Value (USD)')
|
ax1.set_ylabel('Portfolio\nValue\n(USD)')
|
||||||
|
|
||||||
ax2 = plt.subplot(612, sharex=ax1)
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
|
ax2.set_ylabel('{asset}\n(USD)'.format(asset=context.ASSET_NAME))
|
||||||
(context.TICK_SIZE * results[['price']]).plot(ax=ax2)
|
results[['price']].plot(ax=ax2)
|
||||||
|
|
||||||
trans = results.ix[[t != [] for t in results.transactions]]
|
trans = results.ix[[t != [] for t in results.transactions]]
|
||||||
buys = trans.ix[
|
buys = trans.ix[
|
||||||
[t[0]['amount'] > 0 for t in trans.transactions]
|
[t[0]['amount'] > 0 for t in trans.transactions]
|
||||||
]
|
]
|
||||||
ax2.plot(
|
ax2.scatter(
|
||||||
buys.index,
|
buys.index.to_pydatetime(),
|
||||||
context.TICK_SIZE * results.price[buys.index],
|
results.price[buys.index],
|
||||||
'^',
|
marker='^',
|
||||||
markersize=10,
|
s=100,
|
||||||
color='g',
|
c='g',
|
||||||
|
label=''
|
||||||
)
|
)
|
||||||
|
|
||||||
ax3 = plt.subplot(613, sharex=ax1)
|
ax3 = plt.subplot(613, sharex=ax1)
|
||||||
@@ -124,14 +120,29 @@ def analyze(context=None, results=None):
|
|||||||
'algorithm',
|
'algorithm',
|
||||||
'benchmark',
|
'benchmark',
|
||||||
]].plot(ax=ax5)
|
]].plot(ax=ax5)
|
||||||
ax5.set_ylabel('Percent Change')
|
ax5.set_ylabel('Percent\nChange')
|
||||||
|
|
||||||
ax6 = plt.subplot(616, sharex=ax1)
|
ax6 = plt.subplot(616, sharex=ax1)
|
||||||
results[['volume']].plot(ax=ax6)
|
results[['volume']].plot(ax=ax6)
|
||||||
ax6.set_ylabel('Volume (mCoins/5min)')
|
ax6.set_ylabel('Volume')
|
||||||
|
|
||||||
plt.legend(loc=3)
|
plt.legend(loc=3)
|
||||||
|
|
||||||
# Show the plot.
|
# Show the plot.
|
||||||
plt.gcf().set_size_inches(18, 8)
|
plt.gcf().set_size_inches(18, 8)
|
||||||
plt.show()
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=10000,
|
||||||
|
data_frequency='daily',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='buy_and_hodl',
|
||||||
|
base_currency='usdt',
|
||||||
|
start=pd.to_datetime('2015-03-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-31', utc=True),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,8 +1,49 @@
|
|||||||
|
'''
|
||||||
|
This is a very simple example referenced in the beginner's tutorial:
|
||||||
|
https://enigmampc.github.io/catalyst/beginner-tutorial.html
|
||||||
|
|
||||||
|
Run this example, by executing the following from your terminal:
|
||||||
|
catalyst ingest-exchange -x bitfinex -f daily -i btc_usdt
|
||||||
|
catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 \
|
||||||
|
--end 2017-9-30 -o buy_btc_simple_out.pickle
|
||||||
|
|
||||||
|
If you want to run this code using another exchange, make sure that
|
||||||
|
the asset is available on that exchange. For example, if you were to run
|
||||||
|
it for exchange Poloniex, you would need to edit the following line:
|
||||||
|
|
||||||
|
context.asset = symbol('btc_usdt') # note 'usdt' instead of 'usd'
|
||||||
|
|
||||||
|
and specify exchange poloniex as follows:
|
||||||
|
catalyst ingest-exchange -x poloniex -f daily -i btc_usdt
|
||||||
|
catalyst run -f buy_btc_simple.py -x poloniex --start 2016-1-1 \
|
||||||
|
--end 2017-9-30 -o buy_btc_simple_out.pickle
|
||||||
|
|
||||||
|
To see which assets are available on each exchange, visit:
|
||||||
|
https://www.enigma.co/catalyst/status
|
||||||
|
'''
|
||||||
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import order, record, symbol
|
from catalyst.api import order, record, symbol
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.asset = symbol('btc_usd')
|
context.asset = symbol('btc_usdt')
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
order(asset, 1)
|
order(context.asset, 1)
|
||||||
record(btc=data.current(context.asset, 'price'))
|
record(btc=data.current(context.asset, 'price'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=10000,
|
||||||
|
data_frequency='daily',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='buy_and_hodl',
|
||||||
|
base_currency='usdt',
|
||||||
|
start=pd.to_datetime('2015-03-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-31', utc=True),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
'''
|
|
||||||
This algorithm requires an additional library (ta-lib) beyond those required by catalyst.
|
|
||||||
Install it first by running:
|
|
||||||
$ pip install TA-Lib
|
|
||||||
|
|
||||||
If you get build errors like "fatal error: ta-lib/ta_libc.h: No such file or directory"
|
|
||||||
it typically means that it can't find the underlying TA-Lib library and needs to be installed.
|
|
||||||
See https://mrjbq7.github.io/ta-lib/install.html for instructions on how to install
|
|
||||||
the required dependencies.
|
|
||||||
'''
|
|
||||||
|
|
||||||
import talib
|
import talib
|
||||||
|
import pandas as pd
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.api import (
|
from catalyst.api import (
|
||||||
@@ -17,66 +7,59 @@ from catalyst.api import (
|
|||||||
order_target_percent,
|
order_target_percent,
|
||||||
symbol,
|
symbol,
|
||||||
record,
|
record,
|
||||||
get_open_orders,
|
|
||||||
)
|
)
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
algo_namespace = 'buy_low_sell_high_xrp'
|
algo_namespace = 'buy_the_dip_live'
|
||||||
log = Logger(algo_namespace)
|
log = Logger('buy low sell high')
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
log.info('initializing algo')
|
log.info('initializing algo')
|
||||||
context.ASSET_NAME = 'XRP_USD'
|
context.ASSET_NAME = 'btc_usdt'
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
context.TARGET_POSITIONS = 5000
|
context.TARGET_POSITIONS = 30
|
||||||
context.PROFIT_TARGET = 0.1
|
context.PROFIT_TARGET = 0.1
|
||||||
context.SLIPPAGE_ALLOWED = 0.05
|
context.SLIPPAGE_ALLOWED = 0.02
|
||||||
|
|
||||||
context.retry_check_open_orders = 10
|
|
||||||
context.retry_update_portfolio = 10
|
|
||||||
context.retry_order = 5
|
|
||||||
|
|
||||||
context.swallow_errors = True
|
|
||||||
|
|
||||||
context.errors = []
|
context.errors = []
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _handle_data(context, data):
|
def _handle_data(context, data):
|
||||||
|
price = data.current(context.asset, 'price')
|
||||||
|
log.info('got price {price}'.format(price=price))
|
||||||
|
|
||||||
prices = data.history(
|
prices = data.history(
|
||||||
context.asset,
|
context.asset,
|
||||||
fields='price',
|
fields='price',
|
||||||
bar_count=20,
|
bar_count=20,
|
||||||
frequency='15m'
|
frequency='1D'
|
||||||
)
|
)
|
||||||
|
|
||||||
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
||||||
log.info('got rsi: {}'.format(rsi))
|
log.info('got rsi: {}'.format(rsi))
|
||||||
|
|
||||||
# Buying more when RSI is low, this should lower our cost basis
|
# Buying more when RSI is low, this should lower our cost basis
|
||||||
if rsi <= 30:
|
if rsi <= 30:
|
||||||
buy_increment = 50
|
buy_increment = 1
|
||||||
elif rsi <= 40:
|
elif rsi <= 40:
|
||||||
buy_increment = 20
|
buy_increment = 0.5
|
||||||
elif rsi <= 70:
|
elif rsi <= 70:
|
||||||
buy_increment = 5
|
buy_increment = 0.2
|
||||||
else:
|
else:
|
||||||
buy_increment = None
|
buy_increment = 0.1
|
||||||
|
|
||||||
cash = context.portfolio.cash
|
cash = context.portfolio.cash
|
||||||
log.info('base currency available: {cash}'.format(cash=cash))
|
log.info('base currency available: {cash}'.format(cash=cash))
|
||||||
|
|
||||||
price = data.current(context.asset, 'price')
|
|
||||||
log.info('got price {price}'.format(price=price))
|
|
||||||
|
|
||||||
record(
|
record(
|
||||||
price=price,
|
price=price,
|
||||||
rsi=rsi,
|
rsi=rsi,
|
||||||
)
|
)
|
||||||
|
|
||||||
orders = get_open_orders(context.asset)
|
orders = context.blotter.open_orders
|
||||||
if orders:
|
if orders:
|
||||||
log.info('skipping bar until all open orders execute')
|
log.info('skipping bar until all open orders execute')
|
||||||
return
|
return
|
||||||
@@ -100,8 +83,8 @@ def _handle_data(context, data):
|
|||||||
|
|
||||||
if price < cost_basis:
|
if price < cost_basis:
|
||||||
is_buy = True
|
is_buy = True
|
||||||
elif position.amount > 0 and \
|
elif (position.amount > 0
|
||||||
price > cost_basis * (1 + context.PROFIT_TARGET):
|
and price > cost_basis * (1 + context.PROFIT_TARGET)):
|
||||||
profit = (price * position.amount) - (cost_basis * position.amount)
|
profit = (price * position.amount) - (cost_basis * position.amount)
|
||||||
log.info('closing position, taking profit: {}'.format(profit))
|
log.info('closing position, taking profit: {}'.format(profit))
|
||||||
order_target_percent(
|
order_target_percent(
|
||||||
@@ -138,11 +121,11 @@ def _handle_data(context, data):
|
|||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
log.info('handling bar {}'.format(data.current_dt))
|
log.info('handling bar {}'.format(data.current_dt))
|
||||||
try:
|
# try:
|
||||||
_handle_data(context, data)
|
_handle_data(context, data)
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
log.warn('aborting the bar on error {}'.format(e))
|
# log.warn('aborting the bar on error {}'.format(e))
|
||||||
context.errors.append(e)
|
# context.errors.append(e)
|
||||||
|
|
||||||
log.info('completed bar {}, total execution errors {}'.format(
|
log.info('completed bar {}, total execution errors {}'.format(
|
||||||
data.current_dt,
|
data.current_dt,
|
||||||
@@ -156,3 +139,32 @@ def handle_data(context, data):
|
|||||||
def analyze(context, stats):
|
def analyze(context, stats):
|
||||||
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
live = True
|
||||||
|
if live:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1000,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bittrex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=algo_namespace,
|
||||||
|
base_currency='btc',
|
||||||
|
simulate_orders=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=10000,
|
||||||
|
data_frequency='daily',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='buy_and_hodl',
|
||||||
|
base_currency='usdt',
|
||||||
|
start=pd.to_datetime('2015-03-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-31', utc=True),
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
import talib
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
from catalyst.api import (
|
|
||||||
order,
|
|
||||||
order_target_percent,
|
|
||||||
symbol,
|
|
||||||
record,
|
|
||||||
get_open_orders,
|
|
||||||
)
|
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
|
||||||
|
|
||||||
algo_namespace = 'buy_the_dip_live'
|
|
||||||
log = Logger('buy low sell high')
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
log.info('initializing algo')
|
|
||||||
context.ASSET_NAME = 'btc_usdt'
|
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
|
||||||
|
|
||||||
context.TARGET_POSITIONS = 30
|
|
||||||
context.PROFIT_TARGET = 0.1
|
|
||||||
context.SLIPPAGE_ALLOWED = 0.02
|
|
||||||
|
|
||||||
context.retry_check_open_orders = 10
|
|
||||||
context.retry_update_portfolio = 10
|
|
||||||
context.retry_order = 5
|
|
||||||
|
|
||||||
context.errors = []
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_data(context, data):
|
|
||||||
price = data.current(context.asset, 'price')
|
|
||||||
log.info('got price {price}'.format(price=price))
|
|
||||||
|
|
||||||
prices = data.history(
|
|
||||||
context.asset,
|
|
||||||
fields='price',
|
|
||||||
bar_count=20,
|
|
||||||
frequency='1d'
|
|
||||||
)
|
|
||||||
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
|
||||||
log.info('got rsi: {}'.format(rsi))
|
|
||||||
|
|
||||||
# Buying more when RSI is low, this should lower our cost basis
|
|
||||||
if rsi <= 30:
|
|
||||||
buy_increment = 1
|
|
||||||
elif rsi <= 40:
|
|
||||||
buy_increment = 0.5
|
|
||||||
elif rsi <= 70:
|
|
||||||
buy_increment = 0.2
|
|
||||||
else:
|
|
||||||
buy_increment = 0.1
|
|
||||||
|
|
||||||
cash = context.portfolio.cash
|
|
||||||
log.info('base currency available: {cash}'.format(cash=cash))
|
|
||||||
|
|
||||||
record(
|
|
||||||
price=price,
|
|
||||||
rsi=rsi,
|
|
||||||
)
|
|
||||||
|
|
||||||
orders = get_open_orders(context.asset)
|
|
||||||
if orders:
|
|
||||||
log.info('skipping bar until all open orders execute')
|
|
||||||
return
|
|
||||||
|
|
||||||
is_buy = False
|
|
||||||
cost_basis = None
|
|
||||||
if context.asset in context.portfolio.positions:
|
|
||||||
position = context.portfolio.positions[context.asset]
|
|
||||||
|
|
||||||
cost_basis = position.cost_basis
|
|
||||||
log.info(
|
|
||||||
'found {amount} positions with cost basis {cost_basis}'.format(
|
|
||||||
amount=position.amount,
|
|
||||||
cost_basis=cost_basis
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if position.amount >= context.TARGET_POSITIONS:
|
|
||||||
log.info('reached positions target: {}'.format(position.amount))
|
|
||||||
return
|
|
||||||
|
|
||||||
if price < cost_basis:
|
|
||||||
is_buy = True
|
|
||||||
elif position.amount > 0 and \
|
|
||||||
price > cost_basis * (1 + context.PROFIT_TARGET):
|
|
||||||
profit = (price * position.amount) - (cost_basis * position.amount)
|
|
||||||
log.info('closing position, taking profit: {}'.format(profit))
|
|
||||||
order_target_percent(
|
|
||||||
asset=context.asset,
|
|
||||||
target=0,
|
|
||||||
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.info('no buy or sell opportunity found')
|
|
||||||
else:
|
|
||||||
is_buy = True
|
|
||||||
|
|
||||||
if is_buy:
|
|
||||||
if buy_increment is None:
|
|
||||||
log.info('the rsi is too high to consider buying {}'.format(rsi))
|
|
||||||
return
|
|
||||||
|
|
||||||
if price * buy_increment > cash:
|
|
||||||
log.info('not enough base currency to consider buying')
|
|
||||||
return
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'buying position cheaper than cost basis {} < {}'.format(
|
|
||||||
price,
|
|
||||||
cost_basis
|
|
||||||
)
|
|
||||||
)
|
|
||||||
order(
|
|
||||||
asset=context.asset,
|
|
||||||
amount=buy_increment,
|
|
||||||
limit_price=price * (1 + context.SLIPPAGE_ALLOWED)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
log.info('handling bar {}'.format(data.current_dt))
|
|
||||||
# try:
|
|
||||||
_handle_data(context, data)
|
|
||||||
# except Exception as e:
|
|
||||||
# log.warn('aborting the bar on error {}'.format(e))
|
|
||||||
# context.errors.append(e)
|
|
||||||
|
|
||||||
log.info('completed bar {}, total execution errors {}'.format(
|
|
||||||
data.current_dt,
|
|
||||||
len(context.errors)
|
|
||||||
))
|
|
||||||
|
|
||||||
if len(context.errors) > 0:
|
|
||||||
log.info('the errors:\n{}'.format(context.errors))
|
|
||||||
|
|
||||||
|
|
||||||
def analyze(context, stats):
|
|
||||||
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=100000,
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='poloniex',
|
|
||||||
start=pd.to_datetime('2017-5-01', utc=True),
|
|
||||||
end=pd.to_datetime('2017-10-16', utc=True),
|
|
||||||
base_currency='usdt',
|
|
||||||
data_frequency='daily'
|
|
||||||
)
|
|
||||||
# run_algorithm(
|
|
||||||
# initialize=initialize,
|
|
||||||
# handle_data=handle_data,
|
|
||||||
# analyze=analyze,
|
|
||||||
# exchange_name='poloniex',
|
|
||||||
# live=True,
|
|
||||||
# algo_namespace=algo_namespace,
|
|
||||||
# base_currency='btc'
|
|
||||||
# )
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
import talib
|
|
||||||
from logbook import Logger
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
order,
|
|
||||||
order_target_percent,
|
|
||||||
symbol,
|
|
||||||
record,
|
|
||||||
get_open_orders,
|
|
||||||
)
|
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
|
||||||
|
|
||||||
algo_namespace = 'buy_low_sell_high_neo'
|
|
||||||
log = Logger(algo_namespace)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
log.info('initializing algo')
|
|
||||||
context.asset = symbol('neo_btc', 'bitfinex')
|
|
||||||
|
|
||||||
context.TARGET_POSITIONS = 50000
|
|
||||||
context.PROFIT_TARGET = 0.1
|
|
||||||
context.SLIPPAGE_ALLOWED = 0.02
|
|
||||||
|
|
||||||
context.retry_check_open_orders = 10
|
|
||||||
context.retry_update_portfolio = 10
|
|
||||||
context.retry_order = 5
|
|
||||||
|
|
||||||
context.errors = []
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_data(context, data):
|
|
||||||
price = data.current(context.asset, 'close')
|
|
||||||
log.info('got price {price}'.format(price=price))
|
|
||||||
|
|
||||||
if price is None:
|
|
||||||
log.warn('no pricing data')
|
|
||||||
return
|
|
||||||
|
|
||||||
prices = data.history(
|
|
||||||
context.asset,
|
|
||||||
fields='price',
|
|
||||||
bar_count=1,
|
|
||||||
frequency='1m'
|
|
||||||
)
|
|
||||||
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
|
||||||
log.info('got rsi: {}'.format(rsi))
|
|
||||||
|
|
||||||
# Buying more when RSI is low, this should lower our cost basis
|
|
||||||
if rsi <= 30:
|
|
||||||
buy_increment = 1
|
|
||||||
elif rsi <= 40:
|
|
||||||
buy_increment = 0.5
|
|
||||||
elif rsi <= 70:
|
|
||||||
buy_increment = 0.1
|
|
||||||
else:
|
|
||||||
buy_increment = None
|
|
||||||
|
|
||||||
cash = context.portfolio.cash
|
|
||||||
log.info('base currency available: {cash}'.format(cash=cash))
|
|
||||||
|
|
||||||
record(price=price)
|
|
||||||
|
|
||||||
orders = get_open_orders(context.asset)
|
|
||||||
if len(orders) > 0:
|
|
||||||
log.info('skipping bar until all open orders execute')
|
|
||||||
return
|
|
||||||
|
|
||||||
is_buy = False
|
|
||||||
cost_basis = None
|
|
||||||
if context.asset in context.portfolio.positions:
|
|
||||||
position = context.portfolio.positions[context.asset]
|
|
||||||
|
|
||||||
cost_basis = position.cost_basis
|
|
||||||
log.info(
|
|
||||||
'found {amount} positions with cost basis {cost_basis}'.format(
|
|
||||||
amount=position.amount,
|
|
||||||
cost_basis=cost_basis
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if position.amount >= context.TARGET_POSITIONS:
|
|
||||||
log.info('reached positions target: {}'.format(position.amount))
|
|
||||||
return
|
|
||||||
|
|
||||||
if price < cost_basis:
|
|
||||||
is_buy = True
|
|
||||||
elif position.amount > 0 and \
|
|
||||||
price > cost_basis * (1 + context.PROFIT_TARGET):
|
|
||||||
profit = (price * position.amount) - (cost_basis * position.amount)
|
|
||||||
|
|
||||||
log.info('closing position, taking profit: {}'.format(profit))
|
|
||||||
order_target_percent(
|
|
||||||
asset=context.asset,
|
|
||||||
target=0,
|
|
||||||
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.info('no buy or sell opportunity found')
|
|
||||||
else:
|
|
||||||
is_buy = True
|
|
||||||
|
|
||||||
if is_buy:
|
|
||||||
if buy_increment is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if price * buy_increment > cash:
|
|
||||||
log.info('not enough base currency to consider buying')
|
|
||||||
return
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
'buying position cheaper than cost basis {} < {}'.format(
|
|
||||||
price,
|
|
||||||
cost_basis
|
|
||||||
)
|
|
||||||
)
|
|
||||||
limit_price = price * (1 + context.SLIPPAGE_ALLOWED)
|
|
||||||
order(
|
|
||||||
asset=context.asset,
|
|
||||||
amount=buy_increment,
|
|
||||||
limit_price=limit_price
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
log.info('handling bar {}'.format(data.current_dt))
|
|
||||||
# try:
|
|
||||||
_handle_data(context, data)
|
|
||||||
# except Exception as e:
|
|
||||||
# log.warn('aborting the bar on error {}'.format(e))
|
|
||||||
# context.errors.append(e)
|
|
||||||
|
|
||||||
log.info('completed bar {}, total execution errors {}'.format(
|
|
||||||
data.current_dt,
|
|
||||||
len(context.errors)
|
|
||||||
))
|
|
||||||
|
|
||||||
if len(context.errors) > 0:
|
|
||||||
log.info('the errors:\n{}'.format(context.errors))
|
|
||||||
|
|
||||||
|
|
||||||
def analyze(context, stats):
|
|
||||||
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# run_algorithm(
|
|
||||||
# initialize=initialize,
|
|
||||||
# handle_data=handle_data,
|
|
||||||
# analyze=analyze,
|
|
||||||
# exchange_name='bitfinex',
|
|
||||||
# live=True,
|
|
||||||
# algo_namespace=algo_namespace,
|
|
||||||
# base_currency='btc',
|
|
||||||
# live_graph=False
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Backtest
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=250,
|
|
||||||
data_frequency='minute',
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='bitfinex',
|
|
||||||
algo_namespace=algo_namespace,
|
|
||||||
base_currency='btc'
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import (record, symbol, order_target_percent,)
|
||||||
|
from catalyst.exchange.utils.stats_utils import extract_transactions
|
||||||
|
|
||||||
|
NAMESPACE = 'dual_moving_average'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.i = 0
|
||||||
|
context.asset = symbol('ltc_usd')
|
||||||
|
context.base_price = None
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# define the windows for the moving averages
|
||||||
|
short_window = 50
|
||||||
|
long_window = 200
|
||||||
|
|
||||||
|
# Skip as many bars as long_window to properly compute the average
|
||||||
|
context.i += 1
|
||||||
|
if context.i < long_window:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Compute moving averages calling data.history() for each
|
||||||
|
# moving average with the appropriate parameters. We choose to use
|
||||||
|
# minute bars for this simulation -> freq="1m"
|
||||||
|
# Returns a pandas dataframe.
|
||||||
|
short_data = data.history(context.asset,
|
||||||
|
'price',
|
||||||
|
bar_count=short_window,
|
||||||
|
frequency="1T",
|
||||||
|
)
|
||||||
|
short_mavg = short_data.mean()
|
||||||
|
long_data = data.history(context.asset,
|
||||||
|
'price',
|
||||||
|
bar_count=long_window,
|
||||||
|
frequency="1T",
|
||||||
|
)
|
||||||
|
long_mavg = long_data.mean()
|
||||||
|
|
||||||
|
# Let's keep the price of our asset in a more handy variable
|
||||||
|
price = data.current(context.asset, 'price')
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
|
||||||
|
# Save values for later inspection
|
||||||
|
record(price=price,
|
||||||
|
cash=context.portfolio.cash,
|
||||||
|
price_change=price_change,
|
||||||
|
short_mavg=short_mavg,
|
||||||
|
long_mavg=long_mavg)
|
||||||
|
|
||||||
|
# Since we are using limit orders, some orders may not execute immediately
|
||||||
|
# we wait until all orders are executed before considering more trades.
|
||||||
|
orders = context.blotter.open_orders
|
||||||
|
if len(orders) > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Exit if we cannot trade
|
||||||
|
if not data.can_trade(context.asset):
|
||||||
|
return
|
||||||
|
|
||||||
|
# We check what's our position on our portfolio and trade accordingly
|
||||||
|
pos_amount = context.portfolio.positions[context.asset].amount
|
||||||
|
|
||||||
|
# Trading logic
|
||||||
|
if short_mavg > long_mavg and pos_amount == 0:
|
||||||
|
# we buy 100% of our portfolio for this asset
|
||||||
|
order_target_percent(context.asset, 1)
|
||||||
|
elif short_mavg < long_mavg and pos_amount > 0:
|
||||||
|
# we sell all our positions for this asset
|
||||||
|
order_target_percent(context.asset, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, perf):
|
||||||
|
# Get the base_currency that was passed as a parameter to the simulation
|
||||||
|
exchange = list(context.exchanges.values())[0]
|
||||||
|
base_currency = exchange.base_currency.upper()
|
||||||
|
|
||||||
|
# First chart: Plot portfolio value using base_currency
|
||||||
|
ax1 = plt.subplot(411)
|
||||||
|
perf.loc[:, ['portfolio_value']].plot(ax=ax1)
|
||||||
|
ax1.legend_.remove()
|
||||||
|
ax1.set_ylabel('Portfolio Value\n({})'.format(base_currency))
|
||||||
|
start, end = ax1.get_ylim()
|
||||||
|
ax1.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))
|
||||||
|
|
||||||
|
# Second chart: Plot asset price, moving averages and buys/sells
|
||||||
|
ax2 = plt.subplot(412, sharex=ax1)
|
||||||
|
perf.loc[:, ['price', 'short_mavg', 'long_mavg']].plot(
|
||||||
|
ax=ax2,
|
||||||
|
label='Price')
|
||||||
|
ax2.legend_.remove()
|
||||||
|
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||||
|
asset=context.asset.symbol,
|
||||||
|
base=base_currency
|
||||||
|
))
|
||||||
|
start, end = ax2.get_ylim()
|
||||||
|
ax2.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))
|
||||||
|
|
||||||
|
transaction_df = extract_transactions(perf)
|
||||||
|
if not transaction_df.empty:
|
||||||
|
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||||
|
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||||
|
ax2.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index, 'price'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax2.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index, 'price'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
|
||||||
|
# Third chart: Compare percentage change between our portfolio
|
||||||
|
# and the price of the asset
|
||||||
|
ax3 = plt.subplot(413, sharex=ax1)
|
||||||
|
perf.loc[:, ['algorithm_period_return', 'price_change']].plot(ax=ax3)
|
||||||
|
ax3.legend_.remove()
|
||||||
|
ax3.set_ylabel('Percent Change')
|
||||||
|
start, end = ax3.get_ylim()
|
||||||
|
ax3.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))
|
||||||
|
|
||||||
|
# Fourth chart: Plot our cash
|
||||||
|
ax4 = plt.subplot(414, sharex=ax1)
|
||||||
|
perf.cash.plot(ax=ax4)
|
||||||
|
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||||
|
start, end = ax4.get_ylim()
|
||||||
|
ax4.yaxis.set_ticks(np.arange(0, end, end / 5))
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1000,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bitfinex',
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='usd',
|
||||||
|
start=pd.to_datetime('2017-9-22', utc=True),
|
||||||
|
end=pd.to_datetime('2017-9-23', utc=True),
|
||||||
|
)
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2017 Enigma MPC, Inc.
|
|
||||||
# Copyright 2014 Quantopian, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
order_target_percent,
|
|
||||||
record,
|
|
||||||
symbol,
|
|
||||||
get_open_orders,
|
|
||||||
set_max_leverage,
|
|
||||||
schedule_function,
|
|
||||||
date_rules,
|
|
||||||
attach_pipeline,
|
|
||||||
pipeline_output,
|
|
||||||
)
|
|
||||||
|
|
||||||
from catalyst.pipeline import Pipeline
|
|
||||||
from catalyst.pipeline.data import CryptoPricing
|
|
||||||
from catalyst.pipeline.factors.crypto import VWAP
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
context.ASSET_NAME = 'USDT_BTC'
|
|
||||||
context.TARGET_INVESTMENT_RATIO = 0.8
|
|
||||||
context.SHORT_WINDOW = 30
|
|
||||||
context.LONG_WINDOW = 100
|
|
||||||
|
|
||||||
# For all trading pairs in the poloniex bundle, the default denomination
|
|
||||||
# currently supported by Catalyst is 1/1000th of a full coin. Use this
|
|
||||||
# constant to scale the price of up to that of a full coin if desired.
|
|
||||||
context.TICK_SIZE = 1000.0
|
|
||||||
|
|
||||||
context.i = 0
|
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
|
||||||
|
|
||||||
set_max_leverage(1.0)
|
|
||||||
|
|
||||||
attach_pipeline(make_pipeline(context), 'vwap_pipeline')
|
|
||||||
|
|
||||||
schedule_function(
|
|
||||||
rebalance,
|
|
||||||
time_rules=times_rules.every_minute(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def before_trading_start(context, data):
|
|
||||||
context.pipeline_data = pipeline_output('vwap_pipeline')
|
|
||||||
|
|
||||||
def make_pipeline(context):
|
|
||||||
return Pipeline(
|
|
||||||
columns={
|
|
||||||
'price': CryptoPricing.open.latest,
|
|
||||||
'volume': CryptoPricing.volume.latest,
|
|
||||||
'short_mavg': VWAP(window_length=context.SHORT_WINDOW),
|
|
||||||
'long_mavg': VWAP(window_length=context.LONG_WINDOW),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def rebalance(context, data):
|
|
||||||
context.i += 1
|
|
||||||
|
|
||||||
# skip first LONG_WINDOW bars to fill windows
|
|
||||||
if context.i < context.LONG_WINDOW:
|
|
||||||
return
|
|
||||||
|
|
||||||
# get pipeline data for asset of interest
|
|
||||||
pipeline_data = context.pipeline_data
|
|
||||||
pipeline_data = pipeline_data[pipeline_data.index == context.asset].iloc[0]
|
|
||||||
|
|
||||||
# retrieve long and short moving averages from pipeline
|
|
||||||
short_mavg = pipeline_data.short_mavg
|
|
||||||
long_mavg = pipeline_data.long_mavg
|
|
||||||
price = pipeline_data.price
|
|
||||||
volume = pipeline_data.volume
|
|
||||||
|
|
||||||
# check that order has not already been placed
|
|
||||||
open_orders = get_open_orders()
|
|
||||||
if context.asset not in open_orders:
|
|
||||||
# check that the asset of interest can currently be traded
|
|
||||||
if data.can_trade(context.asset):
|
|
||||||
# adjust portfolio based on comparison of long and short vwap
|
|
||||||
if short_mavg > long_mavg:
|
|
||||||
order_target_percent(
|
|
||||||
context.asset,
|
|
||||||
context.TARGET_INVESTMENT_RATIO,
|
|
||||||
)
|
|
||||||
elif short_mavg < long_mavg:
|
|
||||||
order_target_percent(
|
|
||||||
context.asset,
|
|
||||||
0.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
record(
|
|
||||||
price=price,
|
|
||||||
cash=context.portfolio.cash,
|
|
||||||
leverage=context.account.leverage,
|
|
||||||
short_mavg=short_mavg,
|
|
||||||
long_mavg=long_mavg,
|
|
||||||
volume=volume,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def analyze(context=None, results=None):
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
# Plot the portfolio and asset data.
|
|
||||||
ax1 = plt.subplot(611)
|
|
||||||
results[['portfolio_value']].plot(ax=ax1)
|
|
||||||
ax1.set_ylabel('Portfolio value (USD)')
|
|
||||||
|
|
||||||
ax2 = plt.subplot(612, sharex=ax1)
|
|
||||||
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
|
|
||||||
(context.TICK_SIZE*results[['price', 'short_mavg', 'long_mavg']]).plot(ax=ax2)
|
|
||||||
|
|
||||||
trans = results.ix[[t != [] for t in results.transactions]]
|
|
||||||
amounts = [t[0]['amount'] for t in trans.transactions]
|
|
||||||
|
|
||||||
buys = trans.ix[
|
|
||||||
[t[0]['amount'] > 0 for t in trans.transactions]
|
|
||||||
]
|
|
||||||
sells = trans.ix[
|
|
||||||
[t[0]['amount'] < 0 for t in trans.transactions]
|
|
||||||
]
|
|
||||||
|
|
||||||
ax2.plot(
|
|
||||||
buys.index,
|
|
||||||
context.TICK_SIZE * results.price[buys.index],
|
|
||||||
'^',
|
|
||||||
markersize=10,
|
|
||||||
color='g',
|
|
||||||
)
|
|
||||||
ax2.plot(
|
|
||||||
sells.index,
|
|
||||||
context.TICK_SIZE * results.price[sells.index],
|
|
||||||
'v',
|
|
||||||
markersize=10,
|
|
||||||
color='r',
|
|
||||||
)
|
|
||||||
|
|
||||||
ax3 = plt.subplot(613, sharex=ax1)
|
|
||||||
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
|
|
||||||
ax3.set_ylabel('Leverage (USD)')
|
|
||||||
|
|
||||||
ax4 = plt.subplot(614, sharex=ax1)
|
|
||||||
results[['cash']].plot(ax=ax4)
|
|
||||||
ax4.set_ylabel('Cash (USD)')
|
|
||||||
|
|
||||||
results[[
|
|
||||||
'treasury',
|
|
||||||
'algorithm',
|
|
||||||
'benchmark',
|
|
||||||
]] = results[[
|
|
||||||
'treasury_period_return',
|
|
||||||
'algorithm_period_return',
|
|
||||||
'benchmark_period_return',
|
|
||||||
]]
|
|
||||||
|
|
||||||
ax5 = plt.subplot(615, sharex=ax1)
|
|
||||||
results[[
|
|
||||||
'treasury',
|
|
||||||
'algorithm',
|
|
||||||
'benchmark',
|
|
||||||
]].plot(ax=ax5)
|
|
||||||
ax5.set_ylabel('Percent Change')
|
|
||||||
|
|
||||||
ax6 = plt.subplot(616, sharex=ax1)
|
|
||||||
results[['volume']].plot(ax=ax6)
|
|
||||||
ax6.set_ylabel('Volume (mBTC/day)')
|
|
||||||
|
|
||||||
plt.legend(loc=3)
|
|
||||||
|
|
||||||
# Show the plot.
|
|
||||||
plt.gcf().set_size_inches(18, 8)
|
|
||||||
plt.show()
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol, get_dataset
|
||||||
|
|
||||||
|
START = '2017-01-01'
|
||||||
|
END = '2017-12-31'
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
context.github = get_dataset('github')
|
||||||
|
context.github.sort_index(level=0, inplace=True)
|
||||||
|
|
||||||
|
context.zec = data.history(symbol('zec_usdt'),
|
||||||
|
['price', ],
|
||||||
|
bar_count=365,
|
||||||
|
frequency="1d")
|
||||||
|
context.xmr = data.history(symbol('xmr_usdt'),
|
||||||
|
['price', ],
|
||||||
|
bar_count=365,
|
||||||
|
frequency="1d")
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
ax1 = plt.subplot(211)
|
||||||
|
idx = pd.IndexSlice
|
||||||
|
df = context.github.loc[START:END].loc[
|
||||||
|
idx[:, [b'ZEC']], ['commits']].reset_index(
|
||||||
|
level='symbol', drop=True)
|
||||||
|
df.plot(ax=ax1, color='blue')
|
||||||
|
ax1.legend(loc=2)
|
||||||
|
ax1.set_title('Zcash')
|
||||||
|
ax2 = ax1.twinx()
|
||||||
|
context.zec['price'].loc[START:END].plot(ax=ax2, color='green')
|
||||||
|
ax2.legend(loc=1)
|
||||||
|
|
||||||
|
ax3 = plt.subplot(212)
|
||||||
|
idx = pd.IndexSlice
|
||||||
|
df = context.github.loc[START:END].loc[
|
||||||
|
idx[:, [b'XMR']], ['commits']].reset_index(
|
||||||
|
level='symbol', drop=True)
|
||||||
|
df.plot(ax=ax3, color='blue')
|
||||||
|
ax3.legend(loc=2)
|
||||||
|
ax3.set_title('Monero')
|
||||||
|
ax4 = ax3.twinx()
|
||||||
|
context.xmr['price'].loc[START:END].plot(ax=ax4, color='green')
|
||||||
|
ax4.legend(loc=1)
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1000,
|
||||||
|
data_frequency='daily',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='algo-github',
|
||||||
|
base_currency='usdt',
|
||||||
|
live=False,
|
||||||
|
start=pd.to_datetime(END, utc=True),
|
||||||
|
end=pd.to_datetime(END, utc=True),
|
||||||
|
)
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# For this example, we're going to write a simple momentum script. When the
|
||||||
|
# stock goes up quickly, we're going to buy; when it goes down quickly, we're
|
||||||
|
# going to sell. Hopefully we'll ride the waves.
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol, record, order_target_percent, get_dataset
|
||||||
|
from catalyst.exchange.utils.stats_utils import set_print_settings, \
|
||||||
|
get_pretty_stats
|
||||||
|
# We give a name to the algorithm which Catalyst will use to persist its state.
|
||||||
|
# In this example, Catalyst will create the `.catalyst/data/live_algos`
|
||||||
|
# directory. If we stop and start the algorithm, Catalyst will resume its
|
||||||
|
# state using the files included in the folder.
|
||||||
|
from catalyst.utils.paths import ensure_directory
|
||||||
|
|
||||||
|
NAMESPACE = 'mean_reversion_simple'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
|
# To run an algorithm in Catalyst, you need two functions: initialize and
|
||||||
|
# handle_data.
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
# This initialize function sets any data or variables that you'll use in
|
||||||
|
# your algorithm. For instance, you'll want to define the trading pair (or
|
||||||
|
# trading pairs) you want to backtest. You'll also want to define any
|
||||||
|
# parameters or values you're going to use.
|
||||||
|
|
||||||
|
# In our example, we're looking at Neo in Ether.
|
||||||
|
df = get_dataset('testmarketcap2') # type: pd.DataFrame
|
||||||
|
|
||||||
|
# Picking a specific date in our DataFrame
|
||||||
|
first_dt = df.index.get_level_values(0)[0]
|
||||||
|
# Since we use a MultiIndex with date / symbol, picking a date will
|
||||||
|
# result in a new DataFrame for the selected date with a single
|
||||||
|
# symbol index
|
||||||
|
df = df.xs(first_dt, level=0)
|
||||||
|
# Keep only the top coins by market cap
|
||||||
|
df = df.loc[df['market_cap_usd'].isin(df['market_cap_usd'].nlargest(100))]
|
||||||
|
|
||||||
|
set_print_settings()
|
||||||
|
|
||||||
|
df.sort_values(by=['market_cap_usd'], ascending=True, inplace=True)
|
||||||
|
print('the marketplace data:\n{}'.format(df))
|
||||||
|
|
||||||
|
# Pick the 5 assets with the lowest market cap for trading
|
||||||
|
quote_currency = 'eth'
|
||||||
|
exchange = context.exchanges[next(iter(context.exchanges))]
|
||||||
|
symbols = [a.symbol for a in exchange.assets
|
||||||
|
if a.start_date < context.datetime]
|
||||||
|
context.assets = []
|
||||||
|
for currency, price in df['market_cap_usd'].iteritems():
|
||||||
|
if len(context.assets) >= 5:
|
||||||
|
break
|
||||||
|
|
||||||
|
s = '{}_{}'.format(currency.decode('utf-8'), quote_currency)
|
||||||
|
if s in symbols:
|
||||||
|
context.assets.append(symbol(s))
|
||||||
|
|
||||||
|
context.base_price = None
|
||||||
|
context.current_day = None
|
||||||
|
|
||||||
|
context.RSI_OVERSOLD = 55
|
||||||
|
context.RSI_OVERBOUGHT = 60
|
||||||
|
context.CANDLE_SIZE = '5T'
|
||||||
|
|
||||||
|
context.start_time = time.time()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# This handle_data function is where the real work is done. Our data is
|
||||||
|
# minute-level tick data, and each minute is called a frame. This function
|
||||||
|
# runs on each frame of the data.
|
||||||
|
|
||||||
|
# We flag the first period of each day.
|
||||||
|
# Since cryptocurrencies trade 24/7 the `before_trading_starts` handle
|
||||||
|
# would only execute once. This method works with minute and daily
|
||||||
|
# frequencies.
|
||||||
|
today = data.current_dt.floor('1D')
|
||||||
|
if today != context.current_day:
|
||||||
|
context.traded_today = dict()
|
||||||
|
context.current_day = today
|
||||||
|
|
||||||
|
# Preparing dictionaries for asset-level data points
|
||||||
|
volumes = dict()
|
||||||
|
rsis = dict()
|
||||||
|
price_values = dict()
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
|
||||||
|
for asset in context.assets:
|
||||||
|
# We're computing the volume-weighted-average-price of the security
|
||||||
|
# defined above, in the context.assets variable. For this example,
|
||||||
|
# we're using three bars on the 15 min bars.
|
||||||
|
|
||||||
|
# The frequency attribute determine the bar size. We use this
|
||||||
|
# convention for the frequency alias:
|
||||||
|
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
||||||
|
prices = data.history(
|
||||||
|
asset,
|
||||||
|
fields='close',
|
||||||
|
bar_count=50,
|
||||||
|
frequency=context.CANDLE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ta-lib calculates various technical indicator based on price and
|
||||||
|
# volume arrays.
|
||||||
|
|
||||||
|
# In this example, we are comp
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)
|
||||||
|
|
||||||
|
# We need a variable for the current price of the security to compare
|
||||||
|
# to the average. Since we are requesting two fields, data.current()
|
||||||
|
# returns a DataFrame with
|
||||||
|
current = data.current(asset, fields=['close', 'volume'])
|
||||||
|
price = current['close']
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
# if asset not in context.base_price:
|
||||||
|
# context.base_price[asset] = price
|
||||||
|
#
|
||||||
|
# base_price = context.base_price[asset]
|
||||||
|
# price_change = (price - base_price) / base_price
|
||||||
|
|
||||||
|
# Tracking the relevant data
|
||||||
|
volumes[asset] = current['volume']
|
||||||
|
rsis[asset] = rsi[-1]
|
||||||
|
price_values[asset] = price
|
||||||
|
# price_changes[asset] = price_change
|
||||||
|
|
||||||
|
# We are trying to avoid over-trading by limiting our trades to
|
||||||
|
# one per day.
|
||||||
|
if asset in context.traded_today:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Exit if we cannot trade
|
||||||
|
if not data.can_trade(asset):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Another powerful built-in feature of the Catalyst backtester is the
|
||||||
|
# portfolio object. The portfolio object tracks your positions, cash,
|
||||||
|
# cost basis of specific holdings, and more. In this line, we
|
||||||
|
# calculate how long or short our position is at this minute.
|
||||||
|
pos_amount = context.portfolio.positions[asset].amount
|
||||||
|
|
||||||
|
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
|
||||||
|
log.info(
|
||||||
|
'{}: buying - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Set a style for limit orders,
|
||||||
|
limit_price = price * 1.005
|
||||||
|
target = 1.0 / len(context.assets)
|
||||||
|
order_target_percent(
|
||||||
|
asset, target, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today[asset] = True
|
||||||
|
|
||||||
|
elif rsi[-1] >= context.RSI_OVERBOUGHT and pos_amount > 0:
|
||||||
|
log.info(
|
||||||
|
'{}: selling - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
limit_price = price * 0.995
|
||||||
|
order_target_percent(
|
||||||
|
asset, 0, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today[asset] = True
|
||||||
|
|
||||||
|
# Now that we've collected all current data for this frame, we use
|
||||||
|
# the record() method to save it. This data will be available as
|
||||||
|
# a parameter of the analyze() function for further analysis.
|
||||||
|
record(
|
||||||
|
current_price=price_values,
|
||||||
|
volume=volumes,
|
||||||
|
rsi=rsis,
|
||||||
|
cash=cash,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, perf=None):
|
||||||
|
stats = get_pretty_stats(perf)
|
||||||
|
print('the algo stats:\n{}'.format(stats))
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# The execution mode: backtest or live
|
||||||
|
live = False
|
||||||
|
|
||||||
|
if live:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='btc',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=False,
|
||||||
|
stats_output=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
folder = os.path.join(
|
||||||
|
tempfile.gettempdir(), 'catalyst', NAMESPACE
|
||||||
|
)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
timestr = time.strftime('%Y%m%d-%H%M%S')
|
||||||
|
out = os.path.join(folder, '{}.p'.format(timestr))
|
||||||
|
# catalyst run -f catalyst/examples/mean_reversion_simple.py \
|
||||||
|
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
||||||
|
# --data-frequency minute --capital-base 10000
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=100,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='eth',
|
||||||
|
start=pd.to_datetime('2017-10-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-15', utc=True),
|
||||||
|
)
|
||||||
|
log.info('saved perf stats: {}'.format(out))
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
# For this example, we're going to write a simple momentum script. When the
|
||||||
|
# stock goes up quickly, we're going to buy; when it goes down quickly, we're
|
||||||
|
# going to sell. Hopefully we'll ride the waves.
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol, record, order_target_percent, get_open_orders
|
||||||
|
from catalyst.exchange.utils.stats_utils import extract_transactions
|
||||||
|
# We give a name to the algorithm which Catalyst will use to persist its state.
|
||||||
|
# In this example, Catalyst will create the `.catalyst/data/live_algos`
|
||||||
|
# directory. If we stop and start the algorithm, Catalyst will resume its
|
||||||
|
# state using the files included in the folder.
|
||||||
|
from catalyst.utils.paths import ensure_directory
|
||||||
|
|
||||||
|
NAMESPACE = 'mean_reversion_simple'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
|
# To run an algorithm in Catalyst, you need two functions: initialize and
|
||||||
|
# handle_data.
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
# This initialize function sets any data or variables that you'll use in
|
||||||
|
# your algorithm. For instance, you'll want to define the trading pair (or
|
||||||
|
# trading pairs) you want to backtest. You'll also want to define any
|
||||||
|
# parameters or values you're going to use.
|
||||||
|
|
||||||
|
# In our example, we're looking at Neo in Ether.
|
||||||
|
context.market = symbol('bnb_eth')
|
||||||
|
context.base_price = None
|
||||||
|
context.current_day = None
|
||||||
|
|
||||||
|
context.RSI_OVERSOLD = 60
|
||||||
|
context.RSI_OVERBOUGHT = 70
|
||||||
|
context.CANDLE_SIZE = '15T'
|
||||||
|
|
||||||
|
context.start_time = time.time()
|
||||||
|
|
||||||
|
context.set_commission(maker=0.001, taker=0.002)
|
||||||
|
context.set_slippage(spread=0.001)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# This handle_data function is where the real work is done. Our data is
|
||||||
|
# minute-level tick data, and each minute is called a frame. This function
|
||||||
|
# runs on each frame of the data.
|
||||||
|
|
||||||
|
# We flag the first period of each day.
|
||||||
|
# Since cryptocurrencies trade 24/7 the `before_trading_starts` handle
|
||||||
|
# would only execute once. This method works with minute and daily
|
||||||
|
# frequencies.
|
||||||
|
today = data.current_dt.floor('1D')
|
||||||
|
if today != context.current_day:
|
||||||
|
context.traded_today = False
|
||||||
|
context.current_day = today
|
||||||
|
|
||||||
|
# We're computing the volume-weighted-average-price of the security
|
||||||
|
# defined above, in the context.market variable. For this example, we're
|
||||||
|
# using three bars on the 15 min bars.
|
||||||
|
|
||||||
|
# The frequency attribute determine the bar size. We use this convention
|
||||||
|
# for the frequency alias:
|
||||||
|
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
||||||
|
prices = data.history(
|
||||||
|
context.market,
|
||||||
|
fields='close',
|
||||||
|
bar_count=50,
|
||||||
|
frequency=context.CANDLE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ta-lib calculates various technical indicator based on price and
|
||||||
|
# volume arrays.
|
||||||
|
|
||||||
|
# In this example, we are comp
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)
|
||||||
|
|
||||||
|
# We need a variable for the current price of the security to compare to
|
||||||
|
# the average. Since we are requesting two fields, data.current()
|
||||||
|
# returns a DataFrame with
|
||||||
|
current = data.current(context.market, fields=['close', 'volume'])
|
||||||
|
price = current['close']
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
|
||||||
|
# Now that we've collected all current data for this frame, we use
|
||||||
|
# the record() method to save it. This data will be available as
|
||||||
|
# a parameter of the analyze() function for further analysis.
|
||||||
|
|
||||||
|
record(
|
||||||
|
volume=current['volume'],
|
||||||
|
price=price,
|
||||||
|
price_change=price_change,
|
||||||
|
rsi=rsi[-1],
|
||||||
|
cash=cash
|
||||||
|
)
|
||||||
|
# We are trying to avoid over-trading by limiting our trades to
|
||||||
|
# one per day.
|
||||||
|
if context.traded_today:
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: retest with open orders
|
||||||
|
# Since we are using limit orders, some orders may not execute immediately
|
||||||
|
# we wait until all orders are executed before considering more trades.
|
||||||
|
orders = context.blotter.open_orders
|
||||||
|
if len(orders) > 0:
|
||||||
|
log.info('exiting because orders are open: {}'.format(orders))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Exit if we cannot trade
|
||||||
|
if not data.can_trade(context.market):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Another powerful built-in feature of the Catalyst backtester is the
|
||||||
|
# portfolio object. The portfolio object tracks your positions, cash,
|
||||||
|
# cost basis of specific holdings, and more. In this line, we calculate
|
||||||
|
# how long or short our position is at this minute.
|
||||||
|
pos_amount = context.portfolio.positions[context.market].amount
|
||||||
|
|
||||||
|
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
|
||||||
|
log.info(
|
||||||
|
'{}: buying - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Set a style for limit orders,
|
||||||
|
limit_price = price * 1.005
|
||||||
|
order_target_percent(
|
||||||
|
context.market, 1, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today = True
|
||||||
|
|
||||||
|
elif rsi[-1] >= context.RSI_OVERBOUGHT and pos_amount > 0:
|
||||||
|
log.info(
|
||||||
|
'{}: selling - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
limit_price = price * 0.995
|
||||||
|
order_target_percent(
|
||||||
|
context.market, 0, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today = True
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, perf=None):
|
||||||
|
end = time.time()
|
||||||
|
log.info('elapsed time: {}'.format(end - context.start_time))
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
# The base currency of the algo exchange
|
||||||
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
|
||||||
|
# Plot the portfolio value over time.
|
||||||
|
ax1 = plt.subplot(611)
|
||||||
|
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||||
|
ax1.set_ylabel('Portfolio\nValue\n({})'.format(base_currency))
|
||||||
|
|
||||||
|
# Plot the price increase or decrease over time.
|
||||||
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
|
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
||||||
|
|
||||||
|
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||||
|
asset=context.market.symbol, base=base_currency
|
||||||
|
))
|
||||||
|
|
||||||
|
transaction_df = extract_transactions(perf)
|
||||||
|
if not transaction_df.empty:
|
||||||
|
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||||
|
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||||
|
ax2.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index.floor('1 min'), 'price'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax2.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index.floor('1 min'), 'price'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
|
||||||
|
ax4 = plt.subplot(613, sharex=ax1)
|
||||||
|
perf.loc[:, 'cash'].plot(
|
||||||
|
ax=ax4, label='Base Currency ({})'.format(base_currency)
|
||||||
|
)
|
||||||
|
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||||
|
|
||||||
|
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
|
||||||
|
|
||||||
|
ax5 = plt.subplot(614, sharex=ax1)
|
||||||
|
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||||
|
ax5.set_ylabel('Percent\nChange')
|
||||||
|
|
||||||
|
ax6 = plt.subplot(615, sharex=ax1)
|
||||||
|
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
|
||||||
|
ax6.set_ylabel('RSI')
|
||||||
|
ax6.axhline(context.RSI_OVERBOUGHT, color='darkgoldenrod')
|
||||||
|
ax6.axhline(context.RSI_OVERSOLD, color='darkgoldenrod')
|
||||||
|
|
||||||
|
if not transaction_df.empty:
|
||||||
|
ax6.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index.floor('1 min'), 'rsi'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax6.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index.floor('1 min'), 'rsi'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
plt.legend(loc=3)
|
||||||
|
start, end = ax6.get_ylim()
|
||||||
|
ax6.yaxis.set_ticks(np.arange(0, end, end / 5))
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# The execution mode: backtest or live
|
||||||
|
live = True
|
||||||
|
|
||||||
|
if live:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='binance',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='eth',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=False,
|
||||||
|
stats_output=None,
|
||||||
|
# auth_aliases=dict(poloniex='auth2')
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
folder = os.path.join(
|
||||||
|
tempfile.gettempdir(), 'catalyst', NAMESPACE
|
||||||
|
)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
timestr = time.strftime('%Y%m%d-%H%M%S')
|
||||||
|
out = os.path.join(folder, '{}.p'.format(timestr))
|
||||||
|
# catalyst run -f catalyst/examples/mean_reversion_simple.py \
|
||||||
|
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
||||||
|
# --data-frequency minute --capital-base 10000
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.035,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bitfinex',
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='btc',
|
||||||
|
start=pd.to_datetime('2017-10-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-11-10', utc=True),
|
||||||
|
output=out
|
||||||
|
)
|
||||||
|
log.info('saved perf stats: {}'.format(out))
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
# For this example, we're going to write a simple momentum script. When the
|
||||||
|
# stock goes up quickly, we're going to buy; when it goes down quickly, we're
|
||||||
|
# going to sell. Hopefully we'll ride the waves.
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol, record, order_target_percent, get_open_orders
|
||||||
|
from catalyst.exchange.utils.stats_utils import extract_transactions
|
||||||
|
# We give a name to the algorithm which Catalyst will use to persist its state.
|
||||||
|
# In this example, Catalyst will create the `.catalyst/data/live_algos`
|
||||||
|
# directory. If we stop and start the algorithm, Catalyst will resume its
|
||||||
|
# state using the files included in the folder.
|
||||||
|
from catalyst.utils.paths import ensure_directory
|
||||||
|
|
||||||
|
NAMESPACE = 'mean_reversion_simple'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
|
# To run an algorithm in Catalyst, you need two functions: initialize and
|
||||||
|
# handle_data.
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
# This initialize function sets any data or variables that you'll use in
|
||||||
|
# your algorithm. For instance, you'll want to define the trading pair (or
|
||||||
|
# trading pairs) you want to backtest. You'll also want to define any
|
||||||
|
# parameters or values you're going to use.
|
||||||
|
|
||||||
|
# In our example, we're looking at Neo in Ether.
|
||||||
|
context.market = symbol('eth_btc')
|
||||||
|
context.base_price = None
|
||||||
|
context.current_day = None
|
||||||
|
|
||||||
|
context.RSI_OVERSOLD = 50
|
||||||
|
context.RSI_OVERBOUGHT = 60
|
||||||
|
context.CANDLE_SIZE = '5T'
|
||||||
|
|
||||||
|
context.start_time = time.time()
|
||||||
|
|
||||||
|
context.set_commission(maker=0.001, taker=0.002)
|
||||||
|
# context.set_slippage(spread=0.001)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# This handle_data function is where the real work is done. Our data is
|
||||||
|
# minute-level tick data, and each minute is called a frame. This function
|
||||||
|
# runs on each frame of the data.
|
||||||
|
|
||||||
|
# We flag the first period of each day.
|
||||||
|
# Since cryptocurrencies trade 24/7 the `before_trading_starts` handle
|
||||||
|
# would only execute once. This method works with minute and daily
|
||||||
|
# frequencies.
|
||||||
|
today = data.current_dt.floor('1D')
|
||||||
|
if today != context.current_day:
|
||||||
|
context.traded_today = False
|
||||||
|
context.current_day = today
|
||||||
|
|
||||||
|
# We're computing the volume-weighted-average-price of the security
|
||||||
|
# defined above, in the context.market variable. For this example, we're
|
||||||
|
# using three bars on the 15 min bars.
|
||||||
|
|
||||||
|
# The frequency attribute determine the bar size. We use this convention
|
||||||
|
# for the frequency alias:
|
||||||
|
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
||||||
|
prices = data.history(
|
||||||
|
context.market,
|
||||||
|
fields='close',
|
||||||
|
bar_count=50,
|
||||||
|
frequency=context.CANDLE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ta-lib calculates various technical indicator based on price and
|
||||||
|
# volume arrays.
|
||||||
|
|
||||||
|
# In this example, we are comp
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)
|
||||||
|
|
||||||
|
# We need a variable for the current price of the security to compare to
|
||||||
|
# the average. Since we are requesting two fields, data.current()
|
||||||
|
# returns a DataFrame with
|
||||||
|
current = data.current(context.market, fields=['close', 'volume'])
|
||||||
|
price = current['close']
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
|
||||||
|
# Now that we've collected all current data for this frame, we use
|
||||||
|
# the record() method to save it. This data will be available as
|
||||||
|
# a parameter of the analyze() function for further analysis.
|
||||||
|
|
||||||
|
record(
|
||||||
|
volume=current['volume'],
|
||||||
|
price=price,
|
||||||
|
price_change=price_change,
|
||||||
|
rsi=rsi[-1],
|
||||||
|
cash=cash
|
||||||
|
)
|
||||||
|
# We are trying to avoid over-trading by limiting our trades to
|
||||||
|
# one per day.
|
||||||
|
if context.traded_today:
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: retest with open orders
|
||||||
|
# Since we are using limit orders, some orders may not execute immediately
|
||||||
|
# we wait until all orders are executed before considering more trades.
|
||||||
|
orders = get_open_orders(context.market)
|
||||||
|
if len(orders) > 0:
|
||||||
|
log.info('exiting because orders are open: {}'.format(orders))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Exit if we cannot trade
|
||||||
|
if not data.can_trade(context.market):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Another powerful built-in feature of the Catalyst backtester is the
|
||||||
|
# portfolio object. The portfolio object tracks your positions, cash,
|
||||||
|
# cost basis of specific holdings, and more. In this line, we calculate
|
||||||
|
# how long or short our position is at this minute.
|
||||||
|
pos_amount = context.portfolio.positions[context.market].amount
|
||||||
|
|
||||||
|
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
|
||||||
|
log.info(
|
||||||
|
'{}: buying - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Set a style for limit orders,
|
||||||
|
limit_price = price * 1.005
|
||||||
|
order_target_percent(
|
||||||
|
context.market, 1, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today = True
|
||||||
|
|
||||||
|
elif rsi[-1] >= context.RSI_OVERBOUGHT and pos_amount > 0:
|
||||||
|
log.info(
|
||||||
|
'{}: selling - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
limit_price = price * 0.995
|
||||||
|
order_target_percent(
|
||||||
|
context.market, 0, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today = True
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, perf=None):
|
||||||
|
end = time.time()
|
||||||
|
log.info('elapsed time: {}'.format(end - context.start_time))
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
# The base currency of the algo exchange
|
||||||
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
|
||||||
|
# Plot the portfolio value over time.
|
||||||
|
ax1 = plt.subplot(611)
|
||||||
|
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||||
|
ax1.set_ylabel('Portfolio\nValue\n({})'.format(base_currency))
|
||||||
|
|
||||||
|
# Plot the price increase or decrease over time.
|
||||||
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
|
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
||||||
|
|
||||||
|
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||||
|
asset=context.market.symbol, base=base_currency
|
||||||
|
))
|
||||||
|
|
||||||
|
transaction_df = extract_transactions(perf)
|
||||||
|
if not transaction_df.empty:
|
||||||
|
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||||
|
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||||
|
ax2.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index.floor('1 min'), 'price'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax2.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index.floor('1 min'), 'price'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
|
||||||
|
ax4 = plt.subplot(613, sharex=ax1)
|
||||||
|
perf.loc[:, 'cash'].plot(
|
||||||
|
ax=ax4, label='Base Currency ({})'.format(base_currency)
|
||||||
|
)
|
||||||
|
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||||
|
|
||||||
|
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
|
||||||
|
|
||||||
|
ax5 = plt.subplot(614, sharex=ax1)
|
||||||
|
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||||
|
ax5.set_ylabel('Percent\nChange')
|
||||||
|
|
||||||
|
ax6 = plt.subplot(615, sharex=ax1)
|
||||||
|
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
|
||||||
|
ax6.set_ylabel('RSI')
|
||||||
|
ax6.axhline(context.RSI_OVERBOUGHT, color='darkgoldenrod')
|
||||||
|
ax6.axhline(context.RSI_OVERSOLD, color='darkgoldenrod')
|
||||||
|
|
||||||
|
if not transaction_df.empty:
|
||||||
|
ax6.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index.floor('1 min'), 'rsi'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax6.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index.floor('1 min'), 'rsi'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
plt.legend(loc=3)
|
||||||
|
start, end = ax6.get_ylim()
|
||||||
|
ax6.yaxis.set_ticks(np.arange(0, end, end / 5))
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# The execution mode: backtest or live
|
||||||
|
live = False
|
||||||
|
|
||||||
|
if live:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.025,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='btc',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=False,
|
||||||
|
stats_output=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
folder = os.path.join(
|
||||||
|
tempfile.gettempdir(), 'catalyst', NAMESPACE
|
||||||
|
)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
timestr = time.strftime('%Y%m%d-%H%M%S')
|
||||||
|
out = os.path.join(folder, '{}.p'.format(timestr))
|
||||||
|
# catalyst run -f catalyst/examples/mean_reversion_simple.py \
|
||||||
|
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
||||||
|
# --data-frequency minute --capital-base 10000
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.1,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bitfinex',
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='eth',
|
||||||
|
start=pd.to_datetime('2017-10-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-11-10', utc=True),
|
||||||
|
output=out
|
||||||
|
)
|
||||||
|
log.info('saved perf stats: {}'.format(out))
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
'''Use this code to execute a portfolio optimization model. This code
|
||||||
|
will select the portfolio with the maximum Sharpe Ratio. The parameters
|
||||||
|
are set to use 180 days of historical data and rebalance every 30 days.
|
||||||
|
|
||||||
|
This is the code used in the following article:
|
||||||
|
https://blog.enigma.co/markowitz-portfolio-optimization-for-cryptocurrencies-in-catalyst-b23c38652556
|
||||||
|
|
||||||
|
You can run this code using the Python interpreter:
|
||||||
|
|
||||||
|
$ python portfolio_optimization.py
|
||||||
|
'''
|
||||||
|
|
||||||
|
from __future__ import division
|
||||||
|
import os
|
||||||
|
import pytz
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from catalyst.api import record, symbols, order_target_percent
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
|
np.set_printoptions(threshold='nan', suppress=True)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
# Portfolio assets list
|
||||||
|
context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt',
|
||||||
|
'xmr_usdt')
|
||||||
|
context.nassets = len(context.assets)
|
||||||
|
# Set the time window that will be used to compute expected return
|
||||||
|
# and asset correlations
|
||||||
|
context.window = 180
|
||||||
|
# Set the number of days between each portfolio rebalancing
|
||||||
|
context.rebalance_period = 30
|
||||||
|
context.i = 0
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# Only rebalance at the beggining of the algorithm execution and
|
||||||
|
# every multiple of the rebalance period
|
||||||
|
if context.i == 0 or context.i % context.rebalance_period == 0:
|
||||||
|
n = context.window
|
||||||
|
prices = data.history(context.assets, fields='price',
|
||||||
|
bar_count=n + 1, frequency='1d')
|
||||||
|
pr = np.asmatrix(prices)
|
||||||
|
t_prices = prices.iloc[1:n + 1]
|
||||||
|
t_val = t_prices.values
|
||||||
|
tminus_prices = prices.iloc[0:n]
|
||||||
|
tminus_val = tminus_prices.values
|
||||||
|
# Compute daily returns (r)
|
||||||
|
r = np.asmatrix(t_val / tminus_val - 1)
|
||||||
|
# Compute the expected returns of each asset with the average
|
||||||
|
# daily return for the selected time window
|
||||||
|
m = np.asmatrix(np.mean(r, axis=0))
|
||||||
|
# ###
|
||||||
|
stds = np.std(r, axis=0)
|
||||||
|
# Compute excess returns matrix (xr)
|
||||||
|
xr = r - m
|
||||||
|
# Matrix algebra to get variance-covariance matrix
|
||||||
|
cov_m = np.dot(np.transpose(xr), xr) / n
|
||||||
|
# Compute asset correlation matrix (informative only)
|
||||||
|
corr_m = cov_m / np.dot(np.transpose(stds), stds)
|
||||||
|
|
||||||
|
# Define portfolio optimization parameters
|
||||||
|
n_portfolios = 50000
|
||||||
|
results_array = np.zeros((3 + context.nassets, n_portfolios))
|
||||||
|
for p in range(n_portfolios):
|
||||||
|
weights = np.random.random(context.nassets)
|
||||||
|
weights /= np.sum(weights)
|
||||||
|
w = np.asmatrix(weights)
|
||||||
|
p_r = np.sum(np.dot(w, np.transpose(m))) * 365
|
||||||
|
p_std = np.sqrt(np.dot(np.dot(w, cov_m),
|
||||||
|
np.transpose(w))) * np.sqrt(365)
|
||||||
|
|
||||||
|
# store results in results array
|
||||||
|
results_array[0, p] = p_r
|
||||||
|
results_array[1, p] = p_std
|
||||||
|
# store Sharpe Ratio (return / volatility) - risk free rate element
|
||||||
|
# excluded for simplicity
|
||||||
|
results_array[2, p] = results_array[0, p] / results_array[1, p]
|
||||||
|
i = 0
|
||||||
|
for iw in weights:
|
||||||
|
results_array[3 + i, p] = weights[i]
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# convert results array to Pandas DataFrame
|
||||||
|
results_frame = pd.DataFrame(np.transpose(results_array),
|
||||||
|
columns=['r', 'stdev', 'sharpe']
|
||||||
|
+ context.assets)
|
||||||
|
# locate position of portfolio with highest Sharpe Ratio
|
||||||
|
max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()]
|
||||||
|
# locate positon of portfolio with minimum standard deviation
|
||||||
|
# min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()]
|
||||||
|
|
||||||
|
# order optimal weights for each asset
|
||||||
|
for asset in context.assets:
|
||||||
|
if data.can_trade(asset):
|
||||||
|
order_target_percent(asset, max_sharpe_port[asset])
|
||||||
|
|
||||||
|
# create scatter plot coloured by Sharpe Ratio
|
||||||
|
plt.scatter(results_frame.stdev,
|
||||||
|
results_frame.r,
|
||||||
|
c=results_frame.sharpe,
|
||||||
|
cmap='RdYlGn')
|
||||||
|
plt.xlabel('Volatility')
|
||||||
|
plt.ylabel('Returns')
|
||||||
|
plt.colorbar()
|
||||||
|
# plot red star to highlight position of portfolio
|
||||||
|
# with highest Sharpe Ratio
|
||||||
|
plt.scatter(max_sharpe_port[1],
|
||||||
|
max_sharpe_port[0],
|
||||||
|
marker='o',
|
||||||
|
color='b',
|
||||||
|
s=200)
|
||||||
|
# plot green star to highlight position of minimum variance portfolio
|
||||||
|
plt.show()
|
||||||
|
print(max_sharpe_port)
|
||||||
|
record(pr=pr,
|
||||||
|
r=r,
|
||||||
|
m=m,
|
||||||
|
stds=stds,
|
||||||
|
max_sharpe_port=max_sharpe_port,
|
||||||
|
corr_m=corr_m)
|
||||||
|
context.i += 1
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
# Form DataFrame with selected data
|
||||||
|
data = results[['pr', 'r', 'm', 'stds', 'max_sharpe_port', 'corr_m',
|
||||||
|
'portfolio_value']]
|
||||||
|
|
||||||
|
# Save results in CSV file
|
||||||
|
filename = os.path.splitext(os.path.basename(__file__))[0]
|
||||||
|
data.to_csv(filename + '.csv')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Bitcoin data is available from 2015-3-2. Dates vary for other tokens.
|
||||||
|
start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc)
|
||||||
|
end = datetime(2017, 8, 16, 0, 0, 0, 0, pytz.utc)
|
||||||
|
results = run_algorithm(initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
capital_base=100000,
|
||||||
|
base_currency='usdt', )
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst.api import (
|
||||||
|
order,
|
||||||
|
symbol,
|
||||||
|
record,
|
||||||
|
get_open_orders,
|
||||||
|
)
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
|
algo_namespace = 'rsi'
|
||||||
|
log = Logger(algo_namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
log.info('initializing algo')
|
||||||
|
context.asset = symbol('eth_btc')
|
||||||
|
context.base_price = None
|
||||||
|
|
||||||
|
context.MAX_HOLDINGS = 0.2
|
||||||
|
context.RSI_OVERSOLD = 30
|
||||||
|
context.RSI_OVERSOLD_BBANDS = 45
|
||||||
|
context.RSI_OVERBOUGHT_BBANDS = 55
|
||||||
|
context.SLIPPAGE_ALLOWED = 0.03
|
||||||
|
|
||||||
|
context.TARGET = 0.15
|
||||||
|
context.STOP_LOSS = 0.1
|
||||||
|
context.STOP = 0.03
|
||||||
|
context.position = None
|
||||||
|
|
||||||
|
context.last_bar = None
|
||||||
|
|
||||||
|
context.errors = []
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_buy_sell_decision(context, data, signal, price):
|
||||||
|
orders = get_open_orders(context.asset)
|
||||||
|
if len(orders) > 0:
|
||||||
|
log.info('skipping bar until all open orders execute')
|
||||||
|
return
|
||||||
|
|
||||||
|
positions = context.portfolio.positions
|
||||||
|
if context.position is None and context.asset in positions:
|
||||||
|
position = positions[context.asset]
|
||||||
|
context.position = dict(
|
||||||
|
cost_basis=position['cost_basis'],
|
||||||
|
amount=position['amount'],
|
||||||
|
stop=None
|
||||||
|
)
|
||||||
|
|
||||||
|
# action = None
|
||||||
|
if context.position is not None:
|
||||||
|
cost_basis = context.position['cost_basis']
|
||||||
|
amount = context.position['amount']
|
||||||
|
log.info(
|
||||||
|
'found {amount} positions with cost basis {cost_basis}'.format(
|
||||||
|
amount=amount,
|
||||||
|
cost_basis=cost_basis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stop = context.position['stop']
|
||||||
|
|
||||||
|
target = cost_basis * (1 + context.TARGET)
|
||||||
|
if price >= target:
|
||||||
|
context.position['cost_basis'] = price
|
||||||
|
context.position['stop'] = context.STOP
|
||||||
|
|
||||||
|
stop_target = context.STOP_LOSS if stop is None else context.STOP
|
||||||
|
if price < cost_basis * (1 - stop_target):
|
||||||
|
log.info('executing stop loss')
|
||||||
|
order(
|
||||||
|
asset=context.asset,
|
||||||
|
amount=-amount,
|
||||||
|
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
|
||||||
|
)
|
||||||
|
# action = 0
|
||||||
|
context.position = None
|
||||||
|
|
||||||
|
else:
|
||||||
|
if signal == 'long':
|
||||||
|
log.info('opening position')
|
||||||
|
buy_amount = context.MAX_HOLDINGS / price
|
||||||
|
order(
|
||||||
|
asset=context.asset,
|
||||||
|
amount=buy_amount,
|
||||||
|
limit_price=price * (1 + context.SLIPPAGE_ALLOWED),
|
||||||
|
)
|
||||||
|
context.position = dict(
|
||||||
|
cost_basis=price,
|
||||||
|
amount=buy_amount,
|
||||||
|
stop=None
|
||||||
|
)
|
||||||
|
# action = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_data_rsi_only(context, data):
|
||||||
|
price = data.current(context.asset, 'close')
|
||||||
|
log.info('got price {price}'.format(price=price))
|
||||||
|
|
||||||
|
if price is np.nan:
|
||||||
|
log.warn('no pricing data')
|
||||||
|
return
|
||||||
|
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
|
||||||
|
try:
|
||||||
|
prices = data.history(
|
||||||
|
context.asset,
|
||||||
|
fields='price',
|
||||||
|
bar_count=20,
|
||||||
|
frequency='30T'
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('historical data not available: '.format(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=16)[-1]
|
||||||
|
log.info('got rsi {}'.format(rsi))
|
||||||
|
|
||||||
|
signal = None
|
||||||
|
if rsi < context.RSI_OVERSOLD:
|
||||||
|
signal = 'long'
|
||||||
|
|
||||||
|
# Making sure that the price is still current
|
||||||
|
price = data.current(context.asset, 'close')
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
log.info(
|
||||||
|
'base currency available: {cash}, cap: {cap}'.format(
|
||||||
|
cash=cash,
|
||||||
|
cap=context.MAX_HOLDINGS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
volume = data.current(context.asset, 'volume')
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
record(
|
||||||
|
price=price,
|
||||||
|
price_change=price_change,
|
||||||
|
rsi=rsi,
|
||||||
|
volume=volume,
|
||||||
|
cash=cash,
|
||||||
|
starting_cash=context.portfolio.starting_cash,
|
||||||
|
leverage=context.account.leverage,
|
||||||
|
)
|
||||||
|
|
||||||
|
_handle_buy_sell_decision(context, data, signal, price)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
dt = data.current_dt
|
||||||
|
|
||||||
|
if context.last_bar is None or (
|
||||||
|
context.last_bar + timedelta(minutes=15)) <= dt:
|
||||||
|
context.last_bar = dt
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info('BAR {}'.format(dt))
|
||||||
|
try:
|
||||||
|
_handle_data_rsi_only(context, data)
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('aborting the bar on error {}'.format(e))
|
||||||
|
context.errors.append(e)
|
||||||
|
|
||||||
|
if len(context.errors) > 0:
|
||||||
|
log.info('the errors:\n{}'.format(context.errors))
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
# Plot the portfolio and asset data.
|
||||||
|
ax1 = plt.subplot(611)
|
||||||
|
results.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||||
|
ax1.set_ylabel('Portfolio Value ({})'.format(base_currency))
|
||||||
|
|
||||||
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
|
results.loc[:, 'price'].plot(ax=ax2)
|
||||||
|
ax2.set_ylabel('{asset} ({base})'.format(
|
||||||
|
asset=context.asset.symbol, base=base_currency
|
||||||
|
))
|
||||||
|
|
||||||
|
trans = results.loc[[t != [] for t in results.transactions], :]
|
||||||
|
buys = trans.loc[[t[0]['amount'] > 0 for t in trans.transactions], :]
|
||||||
|
sells = trans.loc[[t[0]['amount'] < 0 for t in trans.transactions], :]
|
||||||
|
# buys = results.loc[results['action'] == 1, :]
|
||||||
|
# sells = results.loc[results['action'] == 0, :]
|
||||||
|
|
||||||
|
ax2.plot(
|
||||||
|
buys.index,
|
||||||
|
results.loc[buys.index, 'price'],
|
||||||
|
'^',
|
||||||
|
markersize=10,
|
||||||
|
color='g',
|
||||||
|
)
|
||||||
|
ax2.plot(
|
||||||
|
sells.index,
|
||||||
|
results.loc[sells.index, 'price'],
|
||||||
|
'v',
|
||||||
|
markersize=10,
|
||||||
|
color='r',
|
||||||
|
)
|
||||||
|
|
||||||
|
ax3 = plt.subplot(613, sharex=ax1)
|
||||||
|
results.loc[:, ['alpha', 'beta']].plot(ax=ax3)
|
||||||
|
ax3.set_ylabel('Alpha / Beta ')
|
||||||
|
|
||||||
|
ax4 = plt.subplot(614, sharex=ax1)
|
||||||
|
results.loc[:, ['starting_cash', 'cash']].plot(ax=ax4)
|
||||||
|
ax4.set_ylabel('Base Currency ({})'.format(base_currency))
|
||||||
|
|
||||||
|
results['algorithm'] = results.loc[:, 'algorithm_period_return']
|
||||||
|
|
||||||
|
ax5 = plt.subplot(615, sharex=ax1)
|
||||||
|
results.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||||
|
ax5.set_ylabel('Percent Change')
|
||||||
|
|
||||||
|
ax6 = plt.subplot(616, sharex=ax1)
|
||||||
|
results.loc[:, 'rsi'].plot(ax=ax6)
|
||||||
|
ax6.set_ylabel('RSI')
|
||||||
|
|
||||||
|
ax6.plot(
|
||||||
|
buys.index,
|
||||||
|
results.loc[buys.index, 'rsi'],
|
||||||
|
'^',
|
||||||
|
markersize=10,
|
||||||
|
color='g',
|
||||||
|
)
|
||||||
|
ax6.plot(
|
||||||
|
sells.index,
|
||||||
|
results.loc[sells.index, 'rsi'],
|
||||||
|
'v',
|
||||||
|
markersize=10,
|
||||||
|
color='r',
|
||||||
|
)
|
||||||
|
|
||||||
|
plt.legend(loc=3)
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Backtest
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.5,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace=algo_namespace,
|
||||||
|
base_currency='btc',
|
||||||
|
start=pd.to_datetime('2017-9-1', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-1', utc=True),
|
||||||
|
)
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,51 +1,144 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import talib
|
import talib
|
||||||
|
from logbook import Logger, INFO
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import symbol
|
from catalyst.api import symbol, record
|
||||||
|
from catalyst.exchange.utils.stats_utils import get_pretty_stats, \
|
||||||
|
extract_transactions
|
||||||
|
|
||||||
|
log = Logger('simple_loop', level=INFO)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
print('initializing')
|
log.info('initializing')
|
||||||
context.asset = symbol('xrp_btc')
|
context.asset = symbol('eth_btc')
|
||||||
|
context.base_price = None
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
print('handling bar: {}'.format(data.current_dt))
|
log.info('handling bar: {}'.format(data.current_dt))
|
||||||
|
|
||||||
price = data.current(context.asset, 'close')
|
price = data.current(context.asset, 'close')
|
||||||
print('got price {price}'.format(price=price))
|
log.info('got price {price}'.format(price=price))
|
||||||
|
|
||||||
prices = data.history(
|
prices = data.history(
|
||||||
context.asset,
|
context.asset,
|
||||||
fields='price',
|
fields='price',
|
||||||
bar_count=15,
|
bar_count=20,
|
||||||
frequency='1d'
|
frequency='30T'
|
||||||
)
|
)
|
||||||
|
last_traded = prices.index[-1]
|
||||||
|
log.info('last candle date: {}'.format(last_traded))
|
||||||
|
|
||||||
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
||||||
print('got rsi: {}'.format(rsi))
|
log.info('got rsi: {}'.format(rsi))
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
if context.base_price is None:
|
||||||
|
context.base_price = price
|
||||||
|
|
||||||
|
price_change = (price - context.base_price) / context.base_price
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
|
||||||
|
# Now that we've collected all current data for this frame, we use
|
||||||
|
# the record() method to save it. This data will be available as
|
||||||
|
# a parameter of the analyze() function for further analysis.
|
||||||
|
record(
|
||||||
|
price=price,
|
||||||
|
price_change=price_change,
|
||||||
|
cash=cash
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, perf):
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
log.info('the stats: {}'.format(get_pretty_stats(perf)))
|
||||||
|
|
||||||
|
# The base currency of the algo exchange
|
||||||
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
|
||||||
|
# Plot the portfolio value over time.
|
||||||
|
ax1 = plt.subplot(611)
|
||||||
|
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||||
|
ax1.set_ylabel('Portfolio Value ({})'.format(base_currency))
|
||||||
|
|
||||||
|
# Plot the price increase or decrease over time.
|
||||||
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
|
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
||||||
|
|
||||||
|
ax2.set_ylabel('{asset} ({base})'.format(
|
||||||
|
asset=context.asset.symbol, base=base_currency
|
||||||
|
))
|
||||||
|
|
||||||
|
transaction_df = extract_transactions(perf)
|
||||||
|
if not transaction_df.empty:
|
||||||
|
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||||
|
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||||
|
ax2.scatter(
|
||||||
|
buy_df.index.to_pydatetime(),
|
||||||
|
perf.loc[buy_df.index, 'price'],
|
||||||
|
marker='^',
|
||||||
|
s=100,
|
||||||
|
c='green',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
ax2.scatter(
|
||||||
|
sell_df.index.to_pydatetime(),
|
||||||
|
perf.loc[sell_df.index, 'price'],
|
||||||
|
marker='v',
|
||||||
|
s=100,
|
||||||
|
c='red',
|
||||||
|
label=''
|
||||||
|
)
|
||||||
|
|
||||||
|
ax4 = plt.subplot(613, sharex=ax1)
|
||||||
|
perf.loc[:, 'cash'].plot(
|
||||||
|
ax=ax4, label='Base Currency ({})'.format(base_currency)
|
||||||
|
)
|
||||||
|
ax4.set_ylabel('Cash ({})'.format(base_currency))
|
||||||
|
|
||||||
|
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
|
||||||
|
|
||||||
|
ax5 = plt.subplot(614, sharex=ax1)
|
||||||
|
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||||
|
ax5.set_ylabel('Percent Change')
|
||||||
|
|
||||||
|
plt.legend(loc=3)
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# run_algorithm(
|
if __name__ == '__main__':
|
||||||
# capital_base=250,
|
mode = 'live'
|
||||||
# start=pd.to_datetime('2015-08-01', utc=True),
|
|
||||||
# end=pd.to_datetime('2017-9-30', utc=True),
|
if mode == 'backtest':
|
||||||
# data_frequency='daily',
|
run_algorithm(
|
||||||
# initialize=initialize,
|
capital_base=1,
|
||||||
# handle_data=handle_data,
|
initialize=initialize,
|
||||||
# analyze=None,
|
handle_data=handle_data,
|
||||||
# exchange_name='poloniex',
|
analyze=None,
|
||||||
# algo_namespace='simple_loop',
|
exchange_name='poloniex',
|
||||||
# base_currency='eth'
|
algo_namespace='simple_loop',
|
||||||
# )
|
base_currency='eth',
|
||||||
run_algorithm(
|
data_frequency='minute',
|
||||||
initialize=initialize,
|
start=pd.to_datetime('2017-9-1', utc=True),
|
||||||
handle_data=handle_data,
|
end=pd.to_datetime('2017-12-1', utc=True),
|
||||||
analyze=None,
|
)
|
||||||
exchange_name='bitfinex',
|
else:
|
||||||
live=True,
|
run_algorithm(
|
||||||
algo_namespace='simple_loop',
|
capital_base=1,
|
||||||
base_currency='eth',
|
initialize=initialize,
|
||||||
live_graph=False
|
handle_data=handle_data,
|
||||||
)
|
analyze=None,
|
||||||
|
exchange_name='binance',
|
||||||
|
live=True,
|
||||||
|
algo_namespace='simple_loop',
|
||||||
|
base_currency='eth',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=True
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
"""
|
||||||
|
Requires Catalyst version 0.3.0 or above
|
||||||
|
Tested on Catalyst version 0.3.3
|
||||||
|
|
||||||
|
This example aims to provide an easy way for users to learn how to
|
||||||
|
collect data from any given exchange and select a subset of the available
|
||||||
|
currency pairs for trading. You simply need to specify the exchange and
|
||||||
|
the market (base_currency) that you want to focus on. You will then see
|
||||||
|
how to create a universe of assets, and filter it based the market you
|
||||||
|
desire.
|
||||||
|
|
||||||
|
The example prints out the closing price of all the pairs for a given
|
||||||
|
market in a given exchange every 30 minutes. The example also contains
|
||||||
|
the OHLCV data with minute-resolution for the past seven days which
|
||||||
|
could be used to create indicators. Use this code as the backbone to
|
||||||
|
create your own trading strategy.
|
||||||
|
|
||||||
|
The lookback_date variable is used to ensure data for a coin existed on
|
||||||
|
the lookback period specified.
|
||||||
|
|
||||||
|
To run, execute the following two commands in a terminal (inside catalyst
|
||||||
|
environment). The first one retrieves all the pricing data needed for this
|
||||||
|
script to run (only needs to be run once), and the second one executes this
|
||||||
|
script with the parameters specified in the run_algorithm() call at the end
|
||||||
|
of the file:
|
||||||
|
|
||||||
|
catalyst ingest-exchange -x bitfinex -f minute
|
||||||
|
|
||||||
|
python simple_universe.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import (symbols, )
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.i = -1 # minute counter
|
||||||
|
context.exchange = list(context.exchanges.values())[0].name.lower()
|
||||||
|
context.base_currency = list(context.exchanges.values())[0].base_currency.lower()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
context.i += 1
|
||||||
|
lookback_days = 7 # 7 days
|
||||||
|
|
||||||
|
# current date & time in each iteration formatted into a string
|
||||||
|
now = data.current_dt
|
||||||
|
date, time = now.strftime('%Y-%m-%d %H:%M:%S').split(' ')
|
||||||
|
lookback_date = now - timedelta(days=lookback_days)
|
||||||
|
# keep only the date as a string, discard the time
|
||||||
|
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[0]
|
||||||
|
|
||||||
|
one_day_in_minutes = 1440 # 60 * 24 assumes data_frequency='minute'
|
||||||
|
# update universe everyday at midnight
|
||||||
|
if not context.i % one_day_in_minutes:
|
||||||
|
context.universe = universe(context, lookback_date, date)
|
||||||
|
|
||||||
|
# get data every 30 minutes
|
||||||
|
minutes = 30
|
||||||
|
|
||||||
|
# get lookback_days of history data: that is 'lookback' number of bins
|
||||||
|
lookback = int(one_day_in_minutes / minutes * lookback_days)
|
||||||
|
if not context.i % minutes and context.universe:
|
||||||
|
# we iterate for every pair in the current universe
|
||||||
|
for coin in context.coins:
|
||||||
|
pair = str(coin.symbol)
|
||||||
|
|
||||||
|
# Get 30 minute interval OHLCV data. This is the standard data
|
||||||
|
# required for candlestick or indicators/signals. Return Pandas
|
||||||
|
# DataFrames. 30T means 30-minute re-sampling of one minute data.
|
||||||
|
# Adjust it to your desired time interval as needed.
|
||||||
|
opened = fill(data.history(coin,
|
||||||
|
'open',
|
||||||
|
bar_count=lookback,
|
||||||
|
frequency='30T')).values
|
||||||
|
high = fill(data.history(coin,
|
||||||
|
'high',
|
||||||
|
bar_count=lookback,
|
||||||
|
frequency='30T')).values
|
||||||
|
low = fill(data.history(coin,
|
||||||
|
'low',
|
||||||
|
bar_count=lookback,
|
||||||
|
frequency='30T')).values
|
||||||
|
close = fill(data.history(coin,
|
||||||
|
'price',
|
||||||
|
bar_count=lookback,
|
||||||
|
frequency='30T')).values
|
||||||
|
volume = fill(data.history(coin,
|
||||||
|
'volume',
|
||||||
|
bar_count=lookback,
|
||||||
|
frequency='30T')).values
|
||||||
|
|
||||||
|
# close[-1] is the last value in the set, which is the equivalent
|
||||||
|
# to current price (as in the most recent value)
|
||||||
|
# displays the minute price for each pair every 30 minutes
|
||||||
|
print('{now}: {pair} -\tO:{o},\tH:{h},\tL:{c},\tC{c},'
|
||||||
|
'\tV:{v}'.format(
|
||||||
|
now=now,
|
||||||
|
pair=pair,
|
||||||
|
o=opened[-1],
|
||||||
|
h=high[-1],
|
||||||
|
l=low[-1],
|
||||||
|
c=close[-1],
|
||||||
|
v=volume[-1],
|
||||||
|
))
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# --------------- Insert Your Strategy Here -------------------
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Get the universe for a given exchange and a given base_currency market
|
||||||
|
# Example: Poloniex BTC Market
|
||||||
|
def universe(context, lookback_date, current_date):
|
||||||
|
# get all the pairs for the given exchange
|
||||||
|
json_symbols = get_exchange_symbols(context.exchange)
|
||||||
|
# convert into a DataFrame for easier processing
|
||||||
|
df = pd.DataFrame.from_dict(json_symbols).transpose().astype(str)
|
||||||
|
df['base_currency'] = df.apply(lambda row: row.symbol.split('_')[1],
|
||||||
|
axis=1)
|
||||||
|
df['market_currency'] = df.apply(lambda row: row.symbol.split('_')[0],
|
||||||
|
axis=1)
|
||||||
|
|
||||||
|
# Filter all the pairs to get only the ones for a given base_currency
|
||||||
|
df = df[df['base_currency'] == context.base_currency]
|
||||||
|
|
||||||
|
# Filter all pairs to ensure that pair existed in the current date range
|
||||||
|
df = df[df.start_date < lookback_date]
|
||||||
|
df = df[df.end_daily >= current_date]
|
||||||
|
context.coins = symbols(*df.symbol) # convert all the pairs to symbols
|
||||||
|
|
||||||
|
return df.symbol.tolist()
|
||||||
|
|
||||||
|
|
||||||
|
# Replace all NA, NAN or infinite values with its nearest value
|
||||||
|
def fill(series):
|
||||||
|
if isinstance(series, pd.Series):
|
||||||
|
return series.replace([np.inf, -np.inf], np.nan).ffill().bfill()
|
||||||
|
elif isinstance(series, np.ndarray):
|
||||||
|
return pd.Series(series).replace(
|
||||||
|
[np.inf, -np.inf], np.nan
|
||||||
|
).ffill().bfill().values
|
||||||
|
else:
|
||||||
|
return series
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
start_date = pd.to_datetime('2017-11-10', utc=True)
|
||||||
|
end_date = pd.to_datetime('2017-11-13', utc=True)
|
||||||
|
|
||||||
|
performance = run_algorithm(start=start_date, end=end_date,
|
||||||
|
capital_base=100.0, # amount of base_currency
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
data_frequency='minute',
|
||||||
|
base_currency='btc',
|
||||||
|
live=False,
|
||||||
|
live_graph=False,
|
||||||
|
algo_namespace='simple_universe')
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
# Run Command
|
||||||
|
# catalyst run --start 2017-1-1 --end 2017-11-1 -o talib_simple.pickle \
|
||||||
|
# -f talib_simple.py -x poloniex
|
||||||
|
#
|
||||||
|
# Description
|
||||||
|
# Simple TALib Example showing how to use various indicators
|
||||||
|
# in you strategy. Based loosly on
|
||||||
|
# https://github.com/mellertson/talib-macd-example/blob/master/talib-macd-matplotlib-example.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import talib as ta
|
||||||
|
from logbook import Logger
|
||||||
|
from matplotlib.dates import date2num
|
||||||
|
from matplotlib.finance import candlestick_ohlc
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import (
|
||||||
|
order,
|
||||||
|
order_target_percent,
|
||||||
|
symbol,
|
||||||
|
)
|
||||||
|
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
||||||
|
|
||||||
|
algo_namespace = 'talib_sample'
|
||||||
|
log = Logger(algo_namespace)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
log.info('Starting TALib Simple Example')
|
||||||
|
|
||||||
|
context.ASSET_NAME = 'BTC_USDT'
|
||||||
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
|
context.ORDER_SIZE = 10
|
||||||
|
context.SLIPPAGE_ALLOWED = 0.05
|
||||||
|
|
||||||
|
context.swallow_errors = True
|
||||||
|
context.errors = []
|
||||||
|
|
||||||
|
# Bars to look at per iteration should be bigger than SMA_SLOW
|
||||||
|
context.BARS = 365
|
||||||
|
context.COUNT = 0
|
||||||
|
|
||||||
|
# Technical Analysis Settings
|
||||||
|
context.SMA_FAST = 50
|
||||||
|
context.SMA_SLOW = 100
|
||||||
|
context.RSI_PERIOD = 14
|
||||||
|
context.RSI_OVER_BOUGHT = 80
|
||||||
|
context.RSI_OVER_SOLD = 20
|
||||||
|
context.RSI_AVG_PERIOD = 15
|
||||||
|
context.MACD_FAST = 12
|
||||||
|
context.MACD_SLOW = 26
|
||||||
|
context.MACD_SIGNAL = 9
|
||||||
|
context.STOCH_K = 14
|
||||||
|
context.STOCH_D = 3
|
||||||
|
context.STOCH_OVER_BOUGHT = 80
|
||||||
|
context.STOCH_OVER_SOLD = 20
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_data(context, data):
|
||||||
|
# Get price, open, high, low, close
|
||||||
|
prices = data.history(
|
||||||
|
context.asset,
|
||||||
|
bar_count=context.BARS,
|
||||||
|
fields=['price', 'open', 'high', 'low', 'close'],
|
||||||
|
frequency='1d')
|
||||||
|
|
||||||
|
# Create a analysis data frame
|
||||||
|
analysis = pd.DataFrame(index=prices.index)
|
||||||
|
|
||||||
|
# SMA FAST
|
||||||
|
analysis['sma_f'] = ta.SMA(prices.close.as_matrix(), context.SMA_FAST)
|
||||||
|
# SMA SLOW
|
||||||
|
analysis['sma_s'] = ta.SMA(prices.close.as_matrix(), context.SMA_SLOW)
|
||||||
|
|
||||||
|
# Relative Strength Index
|
||||||
|
analysis['rsi'] = ta.RSI(prices.close.as_matrix(), context.RSI_PERIOD)
|
||||||
|
# RSI SMA
|
||||||
|
analysis['sma_r'] = ta.SMA(analysis.rsi.as_matrix(),
|
||||||
|
context.RSI_AVG_PERIOD)
|
||||||
|
|
||||||
|
# MACD, MACD Signal, MACD Histogram
|
||||||
|
analysis['macd'], analysis['macdSignal'], analysis['macdHist'] = ta.MACD(
|
||||||
|
prices.close.as_matrix(), fastperiod=context.MACD_FAST,
|
||||||
|
slowperiod=context.MACD_SLOW, signalperiod=context.MACD_SIGNAL)
|
||||||
|
|
||||||
|
# Stochastics %K %D
|
||||||
|
# %K = (Current Close - Lowest Low)/(Highest High - Lowest Low) * 100
|
||||||
|
# %D = 3-day SMA of %K
|
||||||
|
analysis['stoch_k'], analysis['stoch_d'] = ta.STOCH(
|
||||||
|
prices.high.as_matrix(), prices.low.as_matrix(),
|
||||||
|
prices.close.as_matrix(), slowk_period=context.STOCH_K,
|
||||||
|
slowd_period=context.STOCH_D)
|
||||||
|
|
||||||
|
# SMA FAST over SLOW Crossover
|
||||||
|
analysis['sma_test'] = np.where(analysis.sma_f > analysis.sma_s, 1, 0)
|
||||||
|
|
||||||
|
# MACD over Signal Crossover
|
||||||
|
analysis['macd_test'] = np.where((analysis.macd > analysis.macdSignal), 1,
|
||||||
|
0)
|
||||||
|
|
||||||
|
# Stochastics OVER BOUGHT & Decreasing
|
||||||
|
analysis['stoch_over_bought'] = np.where(
|
||||||
|
(analysis.stoch_k > context.STOCH_OVER_BOUGHT) & (
|
||||||
|
analysis.stoch_k > analysis.stoch_k.shift(1)), 1, 0)
|
||||||
|
|
||||||
|
# Stochastics OVER SOLD & Increasing
|
||||||
|
analysis['stoch_over_sold'] = np.where(
|
||||||
|
(analysis.stoch_k < context.STOCH_OVER_SOLD) & (
|
||||||
|
analysis.stoch_k > analysis.stoch_k.shift(1)), 1, 0)
|
||||||
|
|
||||||
|
# RSI OVER BOUGHT & Decreasing
|
||||||
|
analysis['rsi_over_bought'] = np.where(
|
||||||
|
(analysis.rsi > context.RSI_OVER_BOUGHT) & (
|
||||||
|
analysis.rsi < analysis.rsi.shift(1)), 1, 0)
|
||||||
|
|
||||||
|
# RSI OVER SOLD & Increasing
|
||||||
|
analysis['rsi_over_sold'] = np.where(
|
||||||
|
(analysis.rsi < context.RSI_OVER_SOLD) & (
|
||||||
|
analysis.rsi > analysis.rsi.shift(1)), 1, 0)
|
||||||
|
|
||||||
|
# Save the prices and analysis to send to analyze
|
||||||
|
context.prices = prices
|
||||||
|
context.analysis = analysis
|
||||||
|
context.price = data.current(context.asset, 'price')
|
||||||
|
|
||||||
|
makeOrders(context, analysis)
|
||||||
|
|
||||||
|
# Log the values of this bar
|
||||||
|
logAnalysis(analysis)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
log.info('handling bar {}'.format(data.current_dt))
|
||||||
|
try:
|
||||||
|
_handle_data(context, data)
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('aborting the bar on error {}'.format(e))
|
||||||
|
context.errors.append(e)
|
||||||
|
|
||||||
|
log.info('completed bar {}, total execution errors {}'.format(
|
||||||
|
data.current_dt,
|
||||||
|
len(context.errors)
|
||||||
|
))
|
||||||
|
|
||||||
|
if len(context.errors) > 0:
|
||||||
|
log.info('the errors:\n{}'.format(context.errors))
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, results):
|
||||||
|
# Save results in CSV file
|
||||||
|
filename = os.path.splitext(os.path.basename('talib_simple'))[0]
|
||||||
|
results.to_csv(filename + '.csv')
|
||||||
|
|
||||||
|
log.info('the daily stats:\n{}'.format(get_pretty_stats(results)))
|
||||||
|
chart(context, context.prices, context.analysis, results)
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def makeOrders(context, analysis):
|
||||||
|
if context.asset in context.portfolio.positions:
|
||||||
|
|
||||||
|
# Current position
|
||||||
|
position = context.portfolio.positions[context.asset]
|
||||||
|
|
||||||
|
if (position == 0):
|
||||||
|
log.info('Position Zero')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Cost Basis
|
||||||
|
cost_basis = position.cost_basis
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'Holdings: {amount} @ {cost_basis}'.format(
|
||||||
|
amount=position.amount,
|
||||||
|
cost_basis=cost_basis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sell when holding and got sell singnal
|
||||||
|
if isSell(context, analysis):
|
||||||
|
profit = (context.price * position.amount) - (
|
||||||
|
cost_basis * position.amount)
|
||||||
|
order_target_percent(
|
||||||
|
asset=context.asset,
|
||||||
|
target=0,
|
||||||
|
limit_price=context.price * (1 - context.SLIPPAGE_ALLOWED),
|
||||||
|
)
|
||||||
|
log.info(
|
||||||
|
'Sold {amount} @ {price} Profit: {profit}'.format(
|
||||||
|
amount=position.amount,
|
||||||
|
price=context.price,
|
||||||
|
profit=profit
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.info('no buy or sell opportunity found')
|
||||||
|
else:
|
||||||
|
# Buy when not holding and got buy signal
|
||||||
|
if isBuy(context, analysis):
|
||||||
|
order(
|
||||||
|
asset=context.asset,
|
||||||
|
amount=context.ORDER_SIZE,
|
||||||
|
limit_price=context.price * (1 + context.SLIPPAGE_ALLOWED)
|
||||||
|
)
|
||||||
|
log.info(
|
||||||
|
'Bought {amount} @ {price}'.format(
|
||||||
|
amount=context.ORDER_SIZE,
|
||||||
|
price=context.price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def isBuy(context, analysis):
|
||||||
|
# Bullish SMA Crossover
|
||||||
|
if (getLast(analysis, 'sma_test') == 1):
|
||||||
|
# Bullish MACD
|
||||||
|
if (getLast(analysis, 'macd_test') == 1):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# # Bullish Stochastics
|
||||||
|
# if(getLast(analysis, 'stoch_over_sold') == 1):
|
||||||
|
# return True
|
||||||
|
|
||||||
|
# # Bullish RSI
|
||||||
|
# if(getLast(analysis, 'rsi_over_sold') == 1):
|
||||||
|
# return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def isSell(context, analysis):
|
||||||
|
# Bearish SMA Crossover
|
||||||
|
if (getLast(analysis, 'sma_test') == 0):
|
||||||
|
# Bearish MACD
|
||||||
|
if (getLast(analysis, 'macd_test') == 0):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# # Bearish Stochastics
|
||||||
|
# if(getLast(analysis, 'stoch_over_bought') == 0):
|
||||||
|
# return True
|
||||||
|
|
||||||
|
# # Bearish RSI
|
||||||
|
# if(getLast(analysis, 'rsi_over_bought') == 0):
|
||||||
|
# return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def chart(context, prices, analysis, results):
|
||||||
|
results.portfolio_value.plot()
|
||||||
|
|
||||||
|
# Data for matplotlib finance plot
|
||||||
|
dates = date2num(prices.index.to_pydatetime())
|
||||||
|
|
||||||
|
# Create the Open High Low Close Tuple
|
||||||
|
prices_ohlc = [tuple([dates[i],
|
||||||
|
prices.open[i],
|
||||||
|
prices.high[i],
|
||||||
|
prices.low[i],
|
||||||
|
prices.close[i]]) for i in range(len(dates))]
|
||||||
|
|
||||||
|
fig = plt.figure(figsize=(14, 18))
|
||||||
|
|
||||||
|
# Draw the candle sticks
|
||||||
|
ax1 = fig.add_subplot(411)
|
||||||
|
ax1.set_ylabel(context.ASSET_NAME, size=20)
|
||||||
|
candlestick_ohlc(ax1, prices_ohlc, width=0.4, colorup='g', colordown='r')
|
||||||
|
|
||||||
|
# Draw Moving Averages
|
||||||
|
analysis.sma_f.plot(ax=ax1, c='r')
|
||||||
|
analysis.sma_s.plot(ax=ax1, c='g')
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
ax2 = fig.add_subplot(412)
|
||||||
|
ax2.set_ylabel('RSI', size=12)
|
||||||
|
analysis.rsi.plot(ax=ax2, c='g',
|
||||||
|
label='Period: ' + str(context.RSI_PERIOD))
|
||||||
|
analysis.sma_r.plot(ax=ax2, c='r',
|
||||||
|
label='MA: ' + str(context.RSI_AVG_PERIOD))
|
||||||
|
ax2.axhline(y=30, c='b')
|
||||||
|
ax2.axhline(y=50, c='black')
|
||||||
|
ax2.axhline(y=70, c='b')
|
||||||
|
ax2.set_ylim([0, 100])
|
||||||
|
handles, labels = ax2.get_legend_handles_labels()
|
||||||
|
ax2.legend(handles, labels)
|
||||||
|
|
||||||
|
# Draw MACD computed with Talib
|
||||||
|
ax3 = fig.add_subplot(413)
|
||||||
|
ax3.set_ylabel('MACD: ' + str(context.MACD_FAST) + ', ' + str(
|
||||||
|
context.MACD_SLOW) + ', ' + str(context.MACD_SIGNAL), size=12)
|
||||||
|
analysis.macd.plot(ax=ax3, color='b', label='Macd')
|
||||||
|
analysis.macdSignal.plot(ax=ax3, color='g', label='Signal')
|
||||||
|
analysis.macdHist.plot(ax=ax3, color='r', label='Hist')
|
||||||
|
ax3.axhline(0, lw=2, color='0')
|
||||||
|
handles, labels = ax3.get_legend_handles_labels()
|
||||||
|
ax3.legend(handles, labels)
|
||||||
|
|
||||||
|
# Stochastic plot
|
||||||
|
ax4 = fig.add_subplot(414)
|
||||||
|
ax4.set_ylabel('Stoch (k,d)', size=12)
|
||||||
|
analysis.stoch_k.plot(ax=ax4, label='stoch_k:' + str(context.STOCH_K),
|
||||||
|
color='r')
|
||||||
|
analysis.stoch_d.plot(ax=ax4, label='stoch_d:' + str(context.STOCH_D),
|
||||||
|
color='g')
|
||||||
|
handles, labels = ax4.get_legend_handles_labels()
|
||||||
|
ax4.legend(handles, labels)
|
||||||
|
ax4.axhline(y=20, c='b')
|
||||||
|
ax4.axhline(y=50, c='black')
|
||||||
|
ax4.axhline(y=80, c='b')
|
||||||
|
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
def logAnalysis(analysis):
|
||||||
|
# Log only the last value in the array
|
||||||
|
log.info('- sma_f: {:.2f}'.format(getLast(analysis, 'sma_f')))
|
||||||
|
log.info('- sma_s: {:.2f}'.format(getLast(analysis, 'sma_s')))
|
||||||
|
|
||||||
|
log.info('- rsi: {:.2f}'.format(getLast(analysis, 'rsi')))
|
||||||
|
log.info('- sma_r: {:.2f}'.format(getLast(analysis, 'sma_r')))
|
||||||
|
|
||||||
|
log.info('- macd: {:.2f}'.format(getLast(analysis, 'macd')))
|
||||||
|
log.info(
|
||||||
|
'- macdSignal: {:.2f}'.format(getLast(analysis, 'macdSignal')))
|
||||||
|
log.info('- macdHist: {:.2f}'.format(getLast(analysis, 'macdHist')))
|
||||||
|
|
||||||
|
log.info('- stoch_k: {:.2f}'.format(getLast(analysis, 'stoch_k')))
|
||||||
|
log.info('- stoch_d: {:.2f}'.format(getLast(analysis, 'stoch_d')))
|
||||||
|
|
||||||
|
log.info('- sma_test: {}'.format(getLast(analysis, 'sma_test')))
|
||||||
|
log.info('- macd_test: {}'.format(getLast(analysis, 'macd_test')))
|
||||||
|
|
||||||
|
log.info('- stoch_over_bought: {}'.format(
|
||||||
|
getLast(analysis, 'stoch_over_bought')))
|
||||||
|
log.info(
|
||||||
|
'- stoch_over_sold: {}'.format(getLast(analysis, 'stoch_over_sold')))
|
||||||
|
|
||||||
|
log.info('- rsi_over_bought: {}'.format(
|
||||||
|
getLast(analysis, 'rsi_over_bought')))
|
||||||
|
log.info(
|
||||||
|
'- rsi_over_sold: {}'.format(getLast(analysis, 'rsi_over_sold')))
|
||||||
|
|
||||||
|
|
||||||
|
def getLast(arr, name):
|
||||||
|
return arr[name][arr[name].index[-1]]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=10000,
|
||||||
|
data_frequency='daily',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
base_currency='usdt',
|
||||||
|
start=pd.to_datetime('2016-11-1', utc=True),
|
||||||
|
end=pd.to_datetime('2017-11-10', utc=True),
|
||||||
|
)
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
from logbook import Logger
|
|
||||||
|
|
||||||
log = Logger('AssetFinderExchange')
|
|
||||||
|
|
||||||
|
|
||||||
class AssetFinderExchange(object):
|
|
||||||
def __init__(self):
|
|
||||||
self._asset_cache = {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sids(self):
|
|
||||||
"""
|
|
||||||
This seems to be used to pre-fetch assets.
|
|
||||||
I don't think that we need this for live-trading.
|
|
||||||
Leaving the list empty.
|
|
||||||
"""
|
|
||||||
return list()
|
|
||||||
|
|
||||||
def retrieve_all(self, sids, default_none=False):
|
|
||||||
"""
|
|
||||||
Retrieve all assets in `sids`.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
sids : iterable of int
|
|
||||||
Assets to retrieve.
|
|
||||||
default_none : bool
|
|
||||||
If True, return None for failed lookups.
|
|
||||||
If False, raise `SidsNotFound`.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
assets : list[Asset or None]
|
|
||||||
A list of the same length as `sids` containing Assets (or Nones)
|
|
||||||
corresponding to the requested sids.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
SidsNotFound
|
|
||||||
When a requested sid is not found and default_none=False.
|
|
||||||
"""
|
|
||||||
for sid in sids:
|
|
||||||
if sid in self._asset_cache:
|
|
||||||
log.info('got asset from cache: {}'.format(sid))
|
|
||||||
else:
|
|
||||||
log.info('fetching asset: {}'.format(sid))
|
|
||||||
return list()
|
|
||||||
|
|
||||||
def lookup_symbol(self, symbol, exchange, as_of_date=None, fuzzy=False):
|
|
||||||
"""Lookup an asset by symbol.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
symbol : str
|
|
||||||
The ticker symbol to resolve.
|
|
||||||
as_of_date : datetime or None
|
|
||||||
Look up the last owner of this symbol as of this datetime.
|
|
||||||
If ``as_of_date`` is None, then this can only resolve the equity
|
|
||||||
if exactly one equity has ever owned the ticker.
|
|
||||||
fuzzy : bool, optional
|
|
||||||
Should fuzzy symbol matching be used? Fuzzy symbol matching
|
|
||||||
attempts to resolve differences in representations for
|
|
||||||
shareclasses. For example, some people may represent the ``A``
|
|
||||||
shareclass of ``BRK`` as ``BRK.A``, where others could write
|
|
||||||
``BRK_A``.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
equity : Asset
|
|
||||||
The equity that held ``symbol`` on the given ``as_of_date``, or the
|
|
||||||
only equity to hold ``symbol`` if ``as_of_date`` is None.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
SymbolNotFound
|
|
||||||
Raised when no equity has ever held the given symbol.
|
|
||||||
MultipleSymbolsFound
|
|
||||||
Raised when no ``as_of_date`` is given and more than one equity
|
|
||||||
has held ``symbol``. This is also raised when ``fuzzy=True`` and
|
|
||||||
there are multiple candidates for the given ``symbol`` on the
|
|
||||||
``as_of_date``.
|
|
||||||
"""
|
|
||||||
log.debug('looking up symbol: {} {}'.format(symbol, exchange.name))
|
|
||||||
|
|
||||||
key = ','.join([exchange.name, symbol])
|
|
||||||
if key in self._asset_cache:
|
|
||||||
return self._asset_cache[key]
|
|
||||||
else:
|
|
||||||
asset = exchange.get_asset(symbol)
|
|
||||||
self._asset_cache[key] = asset
|
|
||||||
return asset
|
|
||||||
@@ -1,691 +0,0 @@
|
|||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import pytz
|
|
||||||
import requests
|
|
||||||
import six
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.exchange.exchange import Exchange
|
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
|
||||||
from catalyst.exchange.exchange_errors import (
|
|
||||||
ExchangeRequestError,
|
|
||||||
InvalidHistoryFrequencyError,
|
|
||||||
InvalidOrderStyle, OrderCancelError)
|
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
|
|
||||||
ExchangeStopLimitOrder, ExchangeStopOrder
|
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
|
||||||
from catalyst.protocol import Account
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
|
|
||||||
download_exchange_symbols
|
|
||||||
|
|
||||||
# Trying to account for REST api instability
|
|
||||||
# https://stackoverflow.com/questions/15431044/can-i-set-max-retries-for-requests-request
|
|
||||||
requests.adapters.DEFAULT_RETRIES = 20
|
|
||||||
|
|
||||||
BITFINEX_URL = 'https://api.bitfinex.com'
|
|
||||||
|
|
||||||
log = Logger('Bitfinex')
|
|
||||||
warning_logger = Logger('AlgoWarning')
|
|
||||||
|
|
||||||
|
|
||||||
class Bitfinex(Exchange):
|
|
||||||
def __init__(self, key, secret, base_currency, portfolio=None):
|
|
||||||
self.url = BITFINEX_URL
|
|
||||||
self.key = key
|
|
||||||
self.secret = secret.encode('UTF-8')
|
|
||||||
self.name = 'bitfinex'
|
|
||||||
self.color = 'green'
|
|
||||||
self.assets = {}
|
|
||||||
self.load_assets()
|
|
||||||
self.base_currency = base_currency
|
|
||||||
self._portfolio = portfolio
|
|
||||||
self.minute_writer = None
|
|
||||||
self.minute_reader = None
|
|
||||||
|
|
||||||
# The candle limit for each request
|
|
||||||
self.num_candles_limit = 1000
|
|
||||||
|
|
||||||
# Max is 90 but playing it safe
|
|
||||||
# https://www.bitfinex.com/posts/188
|
|
||||||
self.max_requests_per_minute = 80
|
|
||||||
self.request_cpt = dict()
|
|
||||||
|
|
||||||
self.bundle = ExchangeBundle(self)
|
|
||||||
|
|
||||||
def _request(self, operation, data, version='v1'):
|
|
||||||
payload_object = {
|
|
||||||
'request': '/{}/{}'.format(version, operation),
|
|
||||||
'nonce': '{0:f}'.format(time.time() * 1000000),
|
|
||||||
# convert to string
|
|
||||||
'options': {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data is None:
|
|
||||||
payload_dict = payload_object
|
|
||||||
else:
|
|
||||||
payload_dict = payload_object.copy()
|
|
||||||
payload_dict.update(data)
|
|
||||||
|
|
||||||
payload_json = json.dumps(payload_dict)
|
|
||||||
if six.PY3:
|
|
||||||
payload = base64.b64encode(bytes(payload_json, 'utf-8'))
|
|
||||||
else:
|
|
||||||
payload = base64.b64encode(payload_json)
|
|
||||||
|
|
||||||
m = hmac.new(self.secret, payload, hashlib.sha384)
|
|
||||||
m = m.hexdigest()
|
|
||||||
|
|
||||||
# headers
|
|
||||||
headers = {
|
|
||||||
'X-BFX-APIKEY': self.key,
|
|
||||||
'X-BFX-PAYLOAD': payload,
|
|
||||||
'X-BFX-SIGNATURE': m
|
|
||||||
}
|
|
||||||
|
|
||||||
if data is None:
|
|
||||||
request = requests.get(
|
|
||||||
'{url}/{version}/{operation}'.format(
|
|
||||||
url=self.url,
|
|
||||||
version=version,
|
|
||||||
operation=operation
|
|
||||||
), data={},
|
|
||||||
headers=headers)
|
|
||||||
else:
|
|
||||||
request = requests.post(
|
|
||||||
'{url}/{version}/{operation}'.format(
|
|
||||||
url=self.url,
|
|
||||||
version=version,
|
|
||||||
operation=operation
|
|
||||||
),
|
|
||||||
headers=headers)
|
|
||||||
|
|
||||||
return request
|
|
||||||
|
|
||||||
def _get_v2_symbol(self, asset):
|
|
||||||
pair = asset.symbol.split('_')
|
|
||||||
symbol = 't' + pair[0].upper() + pair[1].upper()
|
|
||||||
return symbol
|
|
||||||
|
|
||||||
def _get_v2_symbols(self, assets):
|
|
||||||
"""
|
|
||||||
Workaround to support Bitfinex v2
|
|
||||||
TODO: Might require a separate asset dictionary
|
|
||||||
|
|
||||||
:param assets:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
|
|
||||||
v2_symbols = []
|
|
||||||
for asset in assets:
|
|
||||||
v2_symbols.append(self._get_v2_symbol(asset))
|
|
||||||
|
|
||||||
return v2_symbols
|
|
||||||
|
|
||||||
def _create_order(self, order_status):
|
|
||||||
"""
|
|
||||||
Create a Catalyst order object from a Bitfinex order dictionary
|
|
||||||
:param order_status:
|
|
||||||
:return: Order
|
|
||||||
"""
|
|
||||||
if order_status['is_cancelled']:
|
|
||||||
status = ORDER_STATUS.CANCELLED
|
|
||||||
elif not order_status['is_live']:
|
|
||||||
log.info('found executed order {}'.format(order_status))
|
|
||||||
status = ORDER_STATUS.FILLED
|
|
||||||
else:
|
|
||||||
status = ORDER_STATUS.OPEN
|
|
||||||
|
|
||||||
amount = float(order_status['original_amount'])
|
|
||||||
filled = float(order_status['executed_amount'])
|
|
||||||
|
|
||||||
if order_status['side'] == 'sell':
|
|
||||||
amount = -amount
|
|
||||||
filled = -filled
|
|
||||||
|
|
||||||
price = float(order_status['price'])
|
|
||||||
order_type = order_status['type']
|
|
||||||
|
|
||||||
stop_price = None
|
|
||||||
limit_price = None
|
|
||||||
|
|
||||||
# TODO: is this comprehensive enough?
|
|
||||||
if order_type.endswith('limit'):
|
|
||||||
limit_price = price
|
|
||||||
elif order_type.endswith('stop'):
|
|
||||||
stop_price = price
|
|
||||||
|
|
||||||
executed_price = float(order_status['avg_execution_price'])
|
|
||||||
|
|
||||||
# TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it.
|
|
||||||
commission = None
|
|
||||||
|
|
||||||
date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
|
|
||||||
date = pytz.utc.localize(date)
|
|
||||||
order = Order(
|
|
||||||
dt=date,
|
|
||||||
asset=self.assets[order_status['symbol']],
|
|
||||||
amount=amount,
|
|
||||||
stop=stop_price,
|
|
||||||
limit=limit_price,
|
|
||||||
filled=filled,
|
|
||||||
id=str(order_status['id']),
|
|
||||||
commission=commission
|
|
||||||
)
|
|
||||||
order.status = status
|
|
||||||
|
|
||||||
return order, executed_price
|
|
||||||
|
|
||||||
def get_balances(self):
|
|
||||||
log.debug('retrieving wallets balances')
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
response = self._request('balances', None)
|
|
||||||
balances = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'message' in balances:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='unable to fetch balance {}'.format(balances['message'])
|
|
||||||
)
|
|
||||||
|
|
||||||
std_balances = dict()
|
|
||||||
for balance in balances:
|
|
||||||
currency = balance['currency'].lower()
|
|
||||||
std_balances[currency] = float(balance['available'])
|
|
||||||
|
|
||||||
return std_balances
|
|
||||||
|
|
||||||
@property
|
|
||||||
def account(self):
|
|
||||||
account = Account()
|
|
||||||
|
|
||||||
account.settled_cash = None
|
|
||||||
account.accrued_interest = None
|
|
||||||
account.buying_power = None
|
|
||||||
account.equity_with_loan = None
|
|
||||||
account.total_positions_value = None
|
|
||||||
account.total_positions_exposure = None
|
|
||||||
account.regt_equity = None
|
|
||||||
account.regt_margin = None
|
|
||||||
account.initial_margin_requirement = None
|
|
||||||
account.maintenance_margin_requirement = None
|
|
||||||
account.available_funds = None
|
|
||||||
account.excess_liquidity = None
|
|
||||||
account.cushion = None
|
|
||||||
account.day_trades_remaining = None
|
|
||||||
account.leverage = None
|
|
||||||
account.net_leverage = None
|
|
||||||
account.net_liquidation = None
|
|
||||||
|
|
||||||
return account
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_skew(self):
|
|
||||||
# TODO: research the time skew conditions
|
|
||||||
return pd.Timedelta('0s')
|
|
||||||
|
|
||||||
def get_account(self):
|
|
||||||
# TODO: fetch account data and keep in cache
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_candles(self, data_frequency, assets, bar_count=None,
|
|
||||||
start_dt=None, end_dt=None):
|
|
||||||
"""
|
|
||||||
Retrieve OHLVC candles from Bitfinex
|
|
||||||
|
|
||||||
:param data_frequency:
|
|
||||||
:param assets:
|
|
||||||
:param bar_count:
|
|
||||||
:return:
|
|
||||||
|
|
||||||
Available Frequencies
|
|
||||||
---------------------
|
|
||||||
'1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D', '14D',
|
|
||||||
'1M'
|
|
||||||
"""
|
|
||||||
|
|
||||||
freq_match = re.match(r'([0-9].*)(m|h|d)', data_frequency, re.M | re.I)
|
|
||||||
if freq_match:
|
|
||||||
number = int(freq_match.group(1))
|
|
||||||
unit = freq_match.group(2)
|
|
||||||
|
|
||||||
if unit == 'd':
|
|
||||||
converted_unit = 'D'
|
|
||||||
else:
|
|
||||||
converted_unit = unit
|
|
||||||
|
|
||||||
frequency = '{}{}'.format(number, converted_unit)
|
|
||||||
allowed_frequencies = ['1m', '5m', '15m', '30m', '1h', '3h', '6h',
|
|
||||||
'12h', '1D', '7D', '14D', '1M']
|
|
||||||
|
|
||||||
if frequency not in allowed_frequencies:
|
|
||||||
raise InvalidHistoryFrequencyError(
|
|
||||||
frequency=data_frequency
|
|
||||||
)
|
|
||||||
elif data_frequency == 'minute':
|
|
||||||
frequency = '1m'
|
|
||||||
elif data_frequency == 'daily':
|
|
||||||
frequency = '1D'
|
|
||||||
else:
|
|
||||||
raise InvalidHistoryFrequencyError(
|
|
||||||
frequency=data_frequency
|
|
||||||
)
|
|
||||||
|
|
||||||
# Making sure that assets are iterable
|
|
||||||
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
|
||||||
ohlc_map = dict()
|
|
||||||
for asset in asset_list:
|
|
||||||
symbol = self._get_v2_symbol(asset)
|
|
||||||
url = '{url}/v2/candles/trade:{frequency}:{symbol}'.format(
|
|
||||||
url=self.url,
|
|
||||||
frequency=frequency,
|
|
||||||
symbol=symbol
|
|
||||||
)
|
|
||||||
|
|
||||||
if bar_count:
|
|
||||||
is_list = True
|
|
||||||
url += '/hist?limit={}'.format(int(bar_count))
|
|
||||||
|
|
||||||
def get_ms(date):
|
|
||||||
epoch = datetime.datetime.utcfromtimestamp(0)
|
|
||||||
epoch = epoch.replace(tzinfo=pytz.UTC)
|
|
||||||
|
|
||||||
return (date - epoch).total_seconds() * 1000.0
|
|
||||||
|
|
||||||
if start_dt is not None:
|
|
||||||
start_ms = get_ms(start_dt)
|
|
||||||
url += '&start={0:f}'.format(start_ms)
|
|
||||||
|
|
||||||
if end_dt is not None:
|
|
||||||
end_ms = get_ms(end_dt)
|
|
||||||
url += '&end={0:f}'.format(end_ms)
|
|
||||||
|
|
||||||
else:
|
|
||||||
is_list = False
|
|
||||||
url += '/last'
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
response = requests.get(url)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'error' in response.content:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='Unable to retrieve candles: {}'.format(
|
|
||||||
response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
candles = response.json()
|
|
||||||
|
|
||||||
def ohlc_from_candle(candle):
|
|
||||||
last_traded = pd.Timestamp.utcfromtimestamp(
|
|
||||||
candle[0] / 1000.0)
|
|
||||||
last_traded = last_traded.replace(tzinfo=pytz.UTC)
|
|
||||||
ohlc = dict(
|
|
||||||
open=np.float64(candle[1]),
|
|
||||||
high=np.float64(candle[3]),
|
|
||||||
low=np.float64(candle[4]),
|
|
||||||
close=np.float64(candle[2]),
|
|
||||||
volume=np.float64(candle[5]),
|
|
||||||
price=np.float64(candle[2]),
|
|
||||||
last_traded=last_traded
|
|
||||||
)
|
|
||||||
return ohlc
|
|
||||||
|
|
||||||
if is_list:
|
|
||||||
ohlc_bars = []
|
|
||||||
# We can to list candles from old to new
|
|
||||||
for candle in reversed(candles):
|
|
||||||
ohlc = ohlc_from_candle(candle)
|
|
||||||
ohlc_bars.append(ohlc)
|
|
||||||
|
|
||||||
ohlc_map[asset] = ohlc_bars
|
|
||||||
|
|
||||||
else:
|
|
||||||
ohlc = ohlc_from_candle(candles)
|
|
||||||
ohlc_map[asset] = ohlc
|
|
||||||
|
|
||||||
return ohlc_map[assets] \
|
|
||||||
if isinstance(assets, TradingPair) else ohlc_map
|
|
||||||
|
|
||||||
def create_order(self, asset, amount, is_buy, style):
|
|
||||||
"""
|
|
||||||
Creating order on the exchange.
|
|
||||||
|
|
||||||
:param asset:
|
|
||||||
:param amount:
|
|
||||||
:param is_buy:
|
|
||||||
:param style:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
exchange_symbol = self.get_symbol(asset)
|
|
||||||
if isinstance(style, ExchangeLimitOrder) \
|
|
||||||
or isinstance(style, ExchangeStopLimitOrder):
|
|
||||||
price = style.get_limit_price(is_buy)
|
|
||||||
order_type = 'limit'
|
|
||||||
|
|
||||||
elif isinstance(style, ExchangeStopOrder):
|
|
||||||
price = style.get_stop_price(is_buy)
|
|
||||||
order_type = 'stop'
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise InvalidOrderStyle(exchange=self.name,
|
|
||||||
style=style.__class__.__name__)
|
|
||||||
|
|
||||||
req = dict(
|
|
||||||
symbol=exchange_symbol,
|
|
||||||
amount=str(float(abs(amount))),
|
|
||||||
price="{:.20f}".format(float(price)),
|
|
||||||
side='buy' if is_buy else 'sell',
|
|
||||||
type='exchange ' + order_type, # TODO: support margin trades
|
|
||||||
exchange=self.name,
|
|
||||||
is_hidden=False,
|
|
||||||
is_postonly=False,
|
|
||||||
use_all_available=0,
|
|
||||||
ocoorder=False,
|
|
||||||
buy_price_oco=0,
|
|
||||||
sell_price_oco=0
|
|
||||||
)
|
|
||||||
|
|
||||||
date = pd.Timestamp.utcnow()
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
response = self._request('order/new', req)
|
|
||||||
order_status = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'message' in order_status:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='unable to create Bitfinex order {}'.format(
|
|
||||||
order_status['message'])
|
|
||||||
)
|
|
||||||
|
|
||||||
order_id = str(order_status['id'])
|
|
||||||
order = Order(
|
|
||||||
dt=date,
|
|
||||||
asset=asset,
|
|
||||||
amount=amount,
|
|
||||||
stop=style.get_stop_price(is_buy),
|
|
||||||
limit=style.get_limit_price(is_buy),
|
|
||||||
id=order_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return order
|
|
||||||
|
|
||||||
def get_open_orders(self, asset=None):
|
|
||||||
"""Retrieve all of the current open orders.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
asset : Asset
|
|
||||||
If passed and not None, return only the open orders for the given
|
|
||||||
asset instead of all open orders.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
open_orders : dict[list[Order]] or list[Order]
|
|
||||||
If no asset is passed this will return a dict mapping Assets
|
|
||||||
to a list containing all the open orders for the asset.
|
|
||||||
If an asset is passed then this will return a list of the open
|
|
||||||
orders for this asset.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
response = self._request('orders', None)
|
|
||||||
order_statuses = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'message' in order_statuses:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='Unable to retrieve open orders: {}'.format(
|
|
||||||
order_statuses['message'])
|
|
||||||
)
|
|
||||||
|
|
||||||
orders = []
|
|
||||||
for order_status in order_statuses:
|
|
||||||
order, executed_price = self._create_order(order_status)
|
|
||||||
if asset is None or asset == order.sid:
|
|
||||||
orders.append(order)
|
|
||||||
|
|
||||||
return orders
|
|
||||||
|
|
||||||
def get_order(self, order_id):
|
|
||||||
"""Lookup an order based on the order id returned from one of the
|
|
||||||
order functions.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
order_id : str
|
|
||||||
The unique identifier for the order.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
order : Order
|
|
||||||
The order object.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
response = self._request(
|
|
||||||
'order/status', {'order_id': int(order_id)})
|
|
||||||
order_status = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'message' in order_status:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='Unable to retrieve order status: {}'.format(
|
|
||||||
order_status['message'])
|
|
||||||
)
|
|
||||||
return self._create_order(order_status)
|
|
||||||
|
|
||||||
def cancel_order(self, order_param):
|
|
||||||
"""Cancel an open order.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
order_param : str or Order
|
|
||||||
The order_id or order object to cancel.
|
|
||||||
"""
|
|
||||||
order_id = order_param.id \
|
|
||||||
if isinstance(order_param, Order) else order_param
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
response = self._request('order/cancel', {'order_id': order_id})
|
|
||||||
status = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'message' in status:
|
|
||||||
raise OrderCancelError(
|
|
||||||
order_id=order_id,
|
|
||||||
exchange=self.name,
|
|
||||||
error=status['message']
|
|
||||||
)
|
|
||||||
|
|
||||||
def tickers(self, assets):
|
|
||||||
"""
|
|
||||||
Fetch ticket data for assets
|
|
||||||
https://docs.bitfinex.com/v2/reference#rest-public-tickers
|
|
||||||
|
|
||||||
:param assets:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
symbols = self._get_v2_symbols(assets)
|
|
||||||
log.debug('fetching tickers {}'.format(symbols))
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
response = requests.get(
|
|
||||||
'{url}/v2/tickers?symbols={symbols}'.format(
|
|
||||||
url=self.url,
|
|
||||||
symbols=','.join(symbols),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'error' in response.content:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='Unable to retrieve tickers: {}'.format(
|
|
||||||
response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
tickers = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
ticks = dict()
|
|
||||||
for index, ticker in enumerate(tickers):
|
|
||||||
if not len(ticker) == 11:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='Invalid ticker in response: {}'.format(ticker)
|
|
||||||
)
|
|
||||||
|
|
||||||
ticks[assets[index]] = dict(
|
|
||||||
timestamp=pd.Timestamp.utcnow(),
|
|
||||||
bid=ticker[1],
|
|
||||||
ask=ticker[3],
|
|
||||||
last_price=ticker[7],
|
|
||||||
low=ticker[10],
|
|
||||||
high=ticker[9],
|
|
||||||
volume=ticker[8],
|
|
||||||
)
|
|
||||||
|
|
||||||
log.debug('got tickers {}'.format(ticks))
|
|
||||||
return ticks
|
|
||||||
|
|
||||||
def generate_symbols_json(self, filename=None, source_dates=False):
|
|
||||||
symbol_map = {}
|
|
||||||
|
|
||||||
if not source_dates:
|
|
||||||
fn, r = download_exchange_symbols(self.name)
|
|
||||||
with open(fn) as data_file:
|
|
||||||
cached_symbols = json.load(data_file)
|
|
||||||
|
|
||||||
response = self._request('symbols', None)
|
|
||||||
|
|
||||||
for symbol in response.json():
|
|
||||||
if (source_dates):
|
|
||||||
start_date = self.get_symbol_start_date(symbol)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
start_date = cached_symbols[symbol]['start_date']
|
|
||||||
except KeyError as e:
|
|
||||||
start_date = time.strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
try:
|
|
||||||
end_daily = cached_symbols[symbol]['end_daily']
|
|
||||||
except KeyError as e:
|
|
||||||
end_daily = 'N/A'
|
|
||||||
|
|
||||||
try:
|
|
||||||
end_minute = cached_symbols[symbol]['end_minute']
|
|
||||||
except KeyError as e:
|
|
||||||
end_minute = 'N/A'
|
|
||||||
|
|
||||||
symbol_map[symbol] = dict(
|
|
||||||
symbol=symbol[:-3] + '_' + symbol[-3:],
|
|
||||||
start_date=start_date,
|
|
||||||
end_daily=end_daily,
|
|
||||||
end_minute=end_minute,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (filename is None):
|
|
||||||
filename = get_exchange_symbols_filename(self.name)
|
|
||||||
|
|
||||||
with open(filename, 'w') as f:
|
|
||||||
json.dump(symbol_map, f, sort_keys=True, indent=2,
|
|
||||||
separators=(',', ':'))
|
|
||||||
|
|
||||||
def get_symbol_start_date(self, symbol):
|
|
||||||
|
|
||||||
print(symbol)
|
|
||||||
symbol_v2 = 't' + symbol.upper()
|
|
||||||
|
|
||||||
"""
|
|
||||||
For each symbol we retrieve candles with Monhtly resolution
|
|
||||||
We get the first month, and query again with daily resolution
|
|
||||||
around that date, and we get the first date
|
|
||||||
"""
|
|
||||||
url = '{url}/v2/candles/trade:1M:{symbol}/hist'.format(
|
|
||||||
url=self.url,
|
|
||||||
symbol=symbol_v2
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
response = requests.get(url)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
"""
|
|
||||||
If we don't get any data back for our monthly-resolution query
|
|
||||||
it means that symbol started trading less than a month ago, so
|
|
||||||
arbitrarily set the ref. date to 15 days ago to be safe with
|
|
||||||
+/- 31 days
|
|
||||||
"""
|
|
||||||
if (len(response.json())):
|
|
||||||
startmonth = response.json()[-1][0]
|
|
||||||
else:
|
|
||||||
startmonth = int((time.time() - 15 * 24 * 3600) * 1000)
|
|
||||||
|
|
||||||
"""
|
|
||||||
Query again with daily resolution setting the start and end around
|
|
||||||
the startmonth we got above. Avoid end dates greater than now: time.time()
|
|
||||||
"""
|
|
||||||
url = '{url}/v2/candles/trade:1D:{symbol}/hist?start={start}&end={end}'.format(
|
|
||||||
url=self.url,
|
|
||||||
symbol=symbol_v2,
|
|
||||||
start=startmonth - 3600 * 24 * 31 * 1000,
|
|
||||||
end=min(startmonth + 3600 * 24 * 31 * 1000,
|
|
||||||
int(time.time() * 1000))
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
response = requests.get(url)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
return time.strftime('%Y-%m-%d',
|
|
||||||
time.gmtime(int(response.json()[-1][0] / 1000)))
|
|
||||||
|
|
||||||
def get_orderbook(self, asset, order_type='all', limit=100):
|
|
||||||
exchange_symbol = asset.exchange_symbol
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
# TODO: implement limit
|
|
||||||
response = self._request(
|
|
||||||
'book/{}'.format(exchange_symbol), None)
|
|
||||||
data = response.json()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
# TODO: filter by type
|
|
||||||
result = dict()
|
|
||||||
for order_type in data:
|
|
||||||
result[order_type] = []
|
|
||||||
|
|
||||||
for entry in data[order_type]:
|
|
||||||
result[order_type].append(dict(
|
|
||||||
rate=float(entry['price']),
|
|
||||||
quantity=float(entry['amount'])
|
|
||||||
))
|
|
||||||
|
|
||||||
return result
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
{
|
|
||||||
"neobtc": {
|
|
||||||
"symbol": "neo_btc",
|
|
||||||
"start_date": "2017-09-07",
|
|
||||||
"precision": 5
|
|
||||||
},
|
|
||||||
"neousd": {
|
|
||||||
"symbol": "neo_usd",
|
|
||||||
"start_date": "2017-09-07"
|
|
||||||
},
|
|
||||||
"neoeth": {
|
|
||||||
"symbol": "neo_eth",
|
|
||||||
"start_date": "2017-09-07"
|
|
||||||
},
|
|
||||||
"btcusd": {
|
|
||||||
"symbol": "btc_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"bchusd": {
|
|
||||||
"symbol": "bch_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"ltcusd": {
|
|
||||||
"symbol": "ltc_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"ltcbtc": {
|
|
||||||
"symbol": "ltc_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"ethusd": {
|
|
||||||
"symbol": "eth_usd",
|
|
||||||
"start_date": "2017-01-01"
|
|
||||||
},
|
|
||||||
"ethbtc": {
|
|
||||||
"symbol": "eth_btc",
|
|
||||||
"start_date": "2017-01-01"
|
|
||||||
},
|
|
||||||
"etcbtc": {
|
|
||||||
"symbol": "etc_btc",
|
|
||||||
"start_date": "2017-01-01"
|
|
||||||
},
|
|
||||||
"etcusd": {
|
|
||||||
"symbol": "etc_usd",
|
|
||||||
"start_date": "2017-01-01"
|
|
||||||
},
|
|
||||||
"rrtusd": {
|
|
||||||
"symbol": "rrt_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"rrtbtc": {
|
|
||||||
"symbol": "rrt_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"zecusd": {
|
|
||||||
"symbol": "zec_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"zecbtc": {
|
|
||||||
"symbol": "zec_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"xmrusd": {
|
|
||||||
"symbol": "xmr_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"xmrbtc": {
|
|
||||||
"symbol": "xmr_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"dshusd": {
|
|
||||||
"symbol": "dsh_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"dshbtc": {
|
|
||||||
"symbol": "dsh_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"bccbtc": {
|
|
||||||
"symbol": "bcc_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"bcubtc": {
|
|
||||||
"symbol": "bcu_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"bccusd": {
|
|
||||||
"symbol": "bcc_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"bcuusd": {
|
|
||||||
"symbol": "bcu_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"xrpusd": {
|
|
||||||
"symbol": "xrp_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"xrpbtc": {
|
|
||||||
"symbol": "xrp_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"iotusd": {
|
|
||||||
"symbol": "iot_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"iotbtc": {
|
|
||||||
"symbol": "iot_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"ioteth": {
|
|
||||||
"symbol": "iot_eth",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"eosusd": {
|
|
||||||
"symbol": "eos_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"eosbtc": {
|
|
||||||
"symbol": "eos_btc",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"eoseth": {
|
|
||||||
"symbol": "eos_eth",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,392 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
from catalyst.exchange.bittrex.bittrex_api import Bittrex_api
|
|
||||||
from catalyst.exchange.exchange import Exchange
|
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
|
||||||
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
|
||||||
ExchangeRequestError, InvalidOrderStyle, OrderNotFound, OrderCancelError, \
|
|
||||||
CreateOrderError
|
|
||||||
from catalyst.finance.execution import LimitOrder, StopLimitOrder
|
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
|
|
||||||
download_exchange_symbols
|
|
||||||
|
|
||||||
log = Logger('Bittrex')
|
|
||||||
|
|
||||||
URL2 = 'https://bittrex.com/Api/v2.0'
|
|
||||||
|
|
||||||
|
|
||||||
class Bittrex(Exchange):
|
|
||||||
def __init__(self, key, secret, base_currency, portfolio=None):
|
|
||||||
self.api = Bittrex_api(key=key, secret=secret.encode('UTF-8'))
|
|
||||||
self.name = 'bittrex'
|
|
||||||
self.color = 'blue'
|
|
||||||
self.base_currency = base_currency
|
|
||||||
self._portfolio = portfolio
|
|
||||||
|
|
||||||
self.num_candles_limit = 2000
|
|
||||||
|
|
||||||
# Not sure what the rate limit is but trying to play it safe
|
|
||||||
# https://bitcoin.stackexchange.com/questions/53778/bittrex-api-rate-limit
|
|
||||||
self.max_requests_per_minute = 60
|
|
||||||
self.request_cpt = dict()
|
|
||||||
|
|
||||||
self.minute_writer = None
|
|
||||||
self.minute_reader = None
|
|
||||||
|
|
||||||
self.assets = dict()
|
|
||||||
self.load_assets()
|
|
||||||
|
|
||||||
self.bundle = ExchangeBundle(self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def account(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_skew(self):
|
|
||||||
# TODO: research the time skew conditions
|
|
||||||
return pd.Timedelta('0s')
|
|
||||||
|
|
||||||
def sanitize_curency_symbol(self, exchange_symbol):
|
|
||||||
"""
|
|
||||||
Helper method used to build the universal pair.
|
|
||||||
Include any symbol mapping here if appropriate.
|
|
||||||
|
|
||||||
:param exchange_symbol:
|
|
||||||
:return universal_symbol:
|
|
||||||
"""
|
|
||||||
return exchange_symbol.lower()
|
|
||||||
|
|
||||||
def get_balances(self):
|
|
||||||
try:
|
|
||||||
log.debug('retrieving wallet balances')
|
|
||||||
self.ask_request()
|
|
||||||
balances = self.api.getbalances()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
std_balances = dict()
|
|
||||||
try:
|
|
||||||
for balance in balances:
|
|
||||||
currency = balance['Currency'].lower()
|
|
||||||
std_balances[currency] = balance['Available']
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
raise ExchangeRequestError(error=balances)
|
|
||||||
|
|
||||||
return std_balances
|
|
||||||
|
|
||||||
def create_order(self, asset, amount, is_buy, style):
|
|
||||||
log.info('creating {} order'.format('buy' if is_buy else 'sell'))
|
|
||||||
exchange_symbol = self.get_symbol(asset)
|
|
||||||
|
|
||||||
if isinstance(style, LimitOrder) or isinstance(style, StopLimitOrder):
|
|
||||||
if isinstance(style, StopLimitOrder):
|
|
||||||
log.warn('{} will ignore the stop price'.format(self.name))
|
|
||||||
|
|
||||||
price = style.get_limit_price(is_buy)
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
if is_buy:
|
|
||||||
order_status = self.api.buylimit(exchange_symbol, amount,
|
|
||||||
price)
|
|
||||||
else:
|
|
||||||
order_status = self.api.selllimit(exchange_symbol,
|
|
||||||
abs(amount), price)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'uuid' in order_status:
|
|
||||||
order_id = order_status['uuid']
|
|
||||||
order = Order(
|
|
||||||
dt=pd.Timestamp.utcnow(),
|
|
||||||
asset=asset,
|
|
||||||
amount=amount,
|
|
||||||
stop=style.get_stop_price(is_buy),
|
|
||||||
limit=style.get_limit_price(is_buy),
|
|
||||||
id=order_id
|
|
||||||
)
|
|
||||||
return order
|
|
||||||
else:
|
|
||||||
if order_status == 'INSUFFICIENT_FUNDS':
|
|
||||||
log.warn('not enough funds to create order')
|
|
||||||
return None
|
|
||||||
elif order_status == 'DUST_TRADE_DISALLOWED_MIN_VALUE_50K_SAT':
|
|
||||||
log.warn('Your order is too small, order at least 50K'
|
|
||||||
' Satoshi')
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
raise CreateOrderError(
|
|
||||||
exchange=self.name,
|
|
||||||
error=order_status
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise InvalidOrderStyle(exchange=self.name,
|
|
||||||
style=style.__class__.__name__)
|
|
||||||
|
|
||||||
def get_open_orders(self, asset):
|
|
||||||
symbol = self.get_symbol(asset)
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
open_orders = self.api.getopenorders(symbol)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
orders = list()
|
|
||||||
for order_status in open_orders:
|
|
||||||
order = self._create_order(order_status)
|
|
||||||
orders.append(order)
|
|
||||||
|
|
||||||
return orders
|
|
||||||
|
|
||||||
def _create_order(self, order_status):
|
|
||||||
log.info(
|
|
||||||
'creating catalyst order from Bittrex {}'.format(order_status))
|
|
||||||
if order_status['CancelInitiated']:
|
|
||||||
status = ORDER_STATUS.CANCELLED
|
|
||||||
elif order_status['Closed'] is not None:
|
|
||||||
status = ORDER_STATUS.FILLED
|
|
||||||
else:
|
|
||||||
status = ORDER_STATUS.OPEN
|
|
||||||
|
|
||||||
date = pd.to_datetime(order_status['Opened'], utc=True)
|
|
||||||
amount = order_status['Quantity']
|
|
||||||
filled = amount - order_status['QuantityRemaining']
|
|
||||||
order = Order(
|
|
||||||
dt=date,
|
|
||||||
asset=self.assets[order_status['Exchange']],
|
|
||||||
amount=amount,
|
|
||||||
stop=None, # Not yet supported by Bittrex
|
|
||||||
limit=order_status['Limit'],
|
|
||||||
filled=filled,
|
|
||||||
id=order_status['OrderUuid'],
|
|
||||||
commission=order_status['CommissionPaid']
|
|
||||||
)
|
|
||||||
order.status = status
|
|
||||||
|
|
||||||
executed_price = order_status['PricePerUnit']
|
|
||||||
|
|
||||||
return order, executed_price
|
|
||||||
|
|
||||||
def get_order(self, order_id):
|
|
||||||
log.info('retrieving order {}'.format(order_id))
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
order_status = self.api.getorder(order_id)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if order_status is None:
|
|
||||||
raise OrderNotFound(order_id=order_id, exchange=self.name)
|
|
||||||
|
|
||||||
return self._create_order(order_status)
|
|
||||||
|
|
||||||
def cancel_order(self, order_param):
|
|
||||||
order_id = order_param.id \
|
|
||||||
if isinstance(order_param, Order) else order_param
|
|
||||||
log.info('cancelling order {}'.format(order_id))
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
status = self.api.cancel(order_id)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'message' in status:
|
|
||||||
raise OrderCancelError(
|
|
||||||
order_id=order_id,
|
|
||||||
exchange=self.name,
|
|
||||||
error=status['message']
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_candles(self, data_frequency, assets, bar_count=None,
|
|
||||||
start_date=None):
|
|
||||||
"""
|
|
||||||
Supported Intervals
|
|
||||||
-------------------
|
|
||||||
day, oneMin, fiveMin, thirtyMin, hour
|
|
||||||
|
|
||||||
:param data_frequency:
|
|
||||||
:param assets:
|
|
||||||
:param bar_count:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
log.info('retrieving candles')
|
|
||||||
|
|
||||||
if data_frequency == 'minute' or data_frequency == '1m':
|
|
||||||
frequency = 'oneMin'
|
|
||||||
elif data_frequency == '5m':
|
|
||||||
frequency = 'fiveMin'
|
|
||||||
elif data_frequency == '30m':
|
|
||||||
frequency = 'thirtyMin'
|
|
||||||
elif data_frequency == '1h':
|
|
||||||
frequency = 'hour'
|
|
||||||
elif data_frequency == 'daily' or data_frequency == '1D':
|
|
||||||
frequency = 'day'
|
|
||||||
else:
|
|
||||||
raise InvalidHistoryFrequencyError(
|
|
||||||
frequency=data_frequency
|
|
||||||
)
|
|
||||||
|
|
||||||
# Making sure that assets are iterable
|
|
||||||
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
|
||||||
ohlc_map = dict()
|
|
||||||
for asset in asset_list:
|
|
||||||
url = '{url}/pub/market/GetTicks?marketName={symbol}' \
|
|
||||||
'&tickInterval={frequency}&_=1499127220008'.format(
|
|
||||||
url=URL2,
|
|
||||||
symbol=self.get_symbol(asset),
|
|
||||||
frequency=frequency
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = json.loads(urllib.request.urlopen(url).read().decode())
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if data['message']:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='Unable to fetch candles {}'.format(data['message'])
|
|
||||||
)
|
|
||||||
|
|
||||||
candles = data['result']
|
|
||||||
|
|
||||||
def ohlc_from_candle(candle):
|
|
||||||
ohlc = dict(
|
|
||||||
open=candle['O'],
|
|
||||||
high=candle['H'],
|
|
||||||
low=candle['L'],
|
|
||||||
close=candle['C'],
|
|
||||||
volume=candle['V'],
|
|
||||||
price=candle['C'],
|
|
||||||
last_traded=pd.to_datetime(candle['T'], utc=True)
|
|
||||||
)
|
|
||||||
return ohlc
|
|
||||||
|
|
||||||
ordered_candles = list(reversed(candles))
|
|
||||||
if bar_count is None:
|
|
||||||
ohlc_map[asset] = ohlc_from_candle(ordered_candles[0])
|
|
||||||
else:
|
|
||||||
ohlc_bars = []
|
|
||||||
for candle in ordered_candles[:bar_count]:
|
|
||||||
ohlc = ohlc_from_candle(candle)
|
|
||||||
ohlc_bars.append(ohlc)
|
|
||||||
|
|
||||||
ohlc_map[asset] = ohlc_bars
|
|
||||||
|
|
||||||
return ohlc_map[assets] \
|
|
||||||
if isinstance(assets, TradingPair) else ohlc_map
|
|
||||||
|
|
||||||
def tickers(self, assets):
|
|
||||||
"""
|
|
||||||
As of v1.1, Bittrex only allows one ticker at the time.
|
|
||||||
So we have to make multiple calls to fetch multiple assets.
|
|
||||||
|
|
||||||
:param assets:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
log.info('retrieving tickers')
|
|
||||||
|
|
||||||
ticks = dict()
|
|
||||||
for asset in assets:
|
|
||||||
symbol = self.get_symbol(asset)
|
|
||||||
try:
|
|
||||||
self.ask_request()
|
|
||||||
ticker = self.api.getticker(symbol)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
# TODO: catch invalid ticker
|
|
||||||
ticks[asset] = dict(
|
|
||||||
timestamp=pd.Timestamp.utcnow(),
|
|
||||||
bid=ticker['Bid'],
|
|
||||||
ask=ticker['Ask'],
|
|
||||||
last_price=ticker['Last']
|
|
||||||
)
|
|
||||||
|
|
||||||
log.debug('got tickers {}'.format(ticks))
|
|
||||||
return ticks
|
|
||||||
|
|
||||||
def get_account(self):
|
|
||||||
log.info('retrieving account data')
|
|
||||||
pass
|
|
||||||
|
|
||||||
def generate_symbols_json(self, filename=None):
|
|
||||||
symbol_map = {}
|
|
||||||
|
|
||||||
fn, r = download_exchange_symbols(self.name)
|
|
||||||
with open(fn) as data_file:
|
|
||||||
cached_symbols = json.load(data_file)
|
|
||||||
|
|
||||||
markets = self.api.getmarkets()
|
|
||||||
for market in markets:
|
|
||||||
exchange_symbol = market['MarketName']
|
|
||||||
symbol = '{market}_{base}'.format(
|
|
||||||
market=self.sanitize_curency_symbol(market['MarketCurrency']),
|
|
||||||
base=self.sanitize_curency_symbol(market['BaseCurrency'])
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
end_daily = cached_symbols[exchange_symbol]['end_daily']
|
|
||||||
except KeyError as e:
|
|
||||||
end_daily = 'N/A'
|
|
||||||
|
|
||||||
try:
|
|
||||||
end_minute = cached_symbols[exchange_symbol]['end_minute']
|
|
||||||
except KeyError as e:
|
|
||||||
end_minute = 'N/A'
|
|
||||||
|
|
||||||
symbol_map[exchange_symbol] = dict(
|
|
||||||
symbol=symbol,
|
|
||||||
start_date=pd.to_datetime(market['Created'],
|
|
||||||
utc=True).strftime("%Y-%m-%d"),
|
|
||||||
end_daily=end_daily,
|
|
||||||
end_minute=end_minute,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (filename is None):
|
|
||||||
filename = get_exchange_symbols_filename(self.name)
|
|
||||||
|
|
||||||
with open(filename, 'w') as f:
|
|
||||||
json.dump(symbol_map, f, sort_keys=True, indent=2,
|
|
||||||
separators=(',', ':'))
|
|
||||||
|
|
||||||
def get_orderbook(self, asset, order_type='all', limit=100):
|
|
||||||
if order_type == 'all':
|
|
||||||
order_type = 'both'
|
|
||||||
elif order_type == 'bid':
|
|
||||||
order_type = 'buy'
|
|
||||||
elif order_type == 'ask':
|
|
||||||
order_type = 'sell'
|
|
||||||
else:
|
|
||||||
raise ValueError('invalid type')
|
|
||||||
|
|
||||||
exchange_symbol = asset.exchange_symbol
|
|
||||||
data = self.api.getorderbook(
|
|
||||||
market=exchange_symbol,
|
|
||||||
type=order_type,
|
|
||||||
depth=100
|
|
||||||
)
|
|
||||||
|
|
||||||
result = dict()
|
|
||||||
for exchange_type in data:
|
|
||||||
if exchange_type == 'buy':
|
|
||||||
order_type = 'bids'
|
|
||||||
elif exchange_type == 'sell':
|
|
||||||
order_type = 'asks'
|
|
||||||
|
|
||||||
result[order_type] = []
|
|
||||||
for entry in data[exchange_type]:
|
|
||||||
result[order_type].append(dict(
|
|
||||||
rate=entry['Rate'],
|
|
||||||
quantity=entry['Quantity']
|
|
||||||
))
|
|
||||||
|
|
||||||
return result
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
# Workaround for backwards compatibility
|
|
||||||
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
|
|
||||||
urlopen = urllib.request.urlopen
|
|
||||||
|
|
||||||
|
|
||||||
class Bittrex_api(object):
|
|
||||||
def __init__(self, key, secret):
|
|
||||||
self.key = key
|
|
||||||
self.secret = secret
|
|
||||||
self.public = ['getmarkets', 'getcurrencies', 'getticker',
|
|
||||||
'getmarketsummaries', 'getmarketsummary',
|
|
||||||
'getorderbook', 'getmarkethistory']
|
|
||||||
self.market = ['buylimit', 'buymarket', 'selllimit', 'sellmarket',
|
|
||||||
'cancel', 'getopenorders']
|
|
||||||
self.account = ['getbalances', 'getbalance', 'getdepositaddress',
|
|
||||||
'withdraw', 'getorder', 'getorderhistory',
|
|
||||||
'getwithdrawalhistory', 'getdeposithistory']
|
|
||||||
|
|
||||||
def query(self, method, values={}):
|
|
||||||
if method in self.public:
|
|
||||||
url = 'https://bittrex.com/api/v1.1/public/'
|
|
||||||
elif method in self.market:
|
|
||||||
url = 'https://bittrex.com/api/v1.1/market/'
|
|
||||||
elif method in self.account:
|
|
||||||
url = 'https://bittrex.com/api/v1.1/account/'
|
|
||||||
else:
|
|
||||||
return 'Something went wrong, sorry.'
|
|
||||||
|
|
||||||
url += method + '?' + urllib.parse.urlencode(values)
|
|
||||||
|
|
||||||
if method not in self.public:
|
|
||||||
url += '&apikey=' + self.key
|
|
||||||
url += '&nonce=' + str(int(time.time()))
|
|
||||||
signature = hmac.new(self.secret, url, hashlib.sha512).hexdigest()
|
|
||||||
headers = {'apisign': signature}
|
|
||||||
else:
|
|
||||||
headers = {}
|
|
||||||
|
|
||||||
req = urllib.request.Request(url, headers=headers)
|
|
||||||
response = json.loads(urlopen(req).read())
|
|
||||||
|
|
||||||
if response["result"]:
|
|
||||||
return response["result"]
|
|
||||||
else:
|
|
||||||
return response["message"]
|
|
||||||
|
|
||||||
def getmarkets(self):
|
|
||||||
return self.query('getmarkets')
|
|
||||||
|
|
||||||
def getcurrencies(self):
|
|
||||||
return self.query('getcurrencies')
|
|
||||||
|
|
||||||
def getticker(self, market):
|
|
||||||
return self.query('getticker', {'market': market})
|
|
||||||
|
|
||||||
def getmarketsummaries(self):
|
|
||||||
return self.query('getmarketsummaries')
|
|
||||||
|
|
||||||
def getmarketsummary(self, market):
|
|
||||||
return self.query('getmarketsummary', {'market': market})
|
|
||||||
|
|
||||||
def getorderbook(self, market, type, depth=20):
|
|
||||||
return self.query('getorderbook',
|
|
||||||
{'market': market, 'type': type, 'depth': depth})
|
|
||||||
|
|
||||||
def getmarkethistory(self, market, count=20):
|
|
||||||
return self.query('getmarkethistory',
|
|
||||||
{'market': market, 'count': count})
|
|
||||||
|
|
||||||
def buylimit(self, market, quantity, rate):
|
|
||||||
return self.query('buylimit', {'market': market, 'quantity': quantity,
|
|
||||||
'rate': rate})
|
|
||||||
|
|
||||||
def buymarket(self, market, quantity):
|
|
||||||
return self.query('buymarket',
|
|
||||||
{'market': market, 'quantity': quantity})
|
|
||||||
|
|
||||||
def selllimit(self, market, quantity, rate):
|
|
||||||
return self.query('selllimit', {'market': market, 'quantity': quantity,
|
|
||||||
'rate': rate})
|
|
||||||
|
|
||||||
def sellmarket(self, market, quantity):
|
|
||||||
return self.query('sellmarket',
|
|
||||||
{'market': market, 'quantity': quantity})
|
|
||||||
|
|
||||||
def cancel(self, uuid):
|
|
||||||
return self.query('cancel', {'uuid': uuid})
|
|
||||||
|
|
||||||
def getopenorders(self, market):
|
|
||||||
return self.query('getopenorders', {'market': market})
|
|
||||||
|
|
||||||
def getbalances(self):
|
|
||||||
return self.query('getbalances')
|
|
||||||
|
|
||||||
def getbalance(self, currency):
|
|
||||||
return self.query('getbalance', {'currency': currency})
|
|
||||||
|
|
||||||
def getdepositaddress(self, currency):
|
|
||||||
return self.query('getdepositaddress', {'currency': currency})
|
|
||||||
|
|
||||||
def withdraw(self, currency, quantity, address):
|
|
||||||
return self.query('withdraw',
|
|
||||||
{'currency': currency, 'quantity': quantity,
|
|
||||||
'address': address})
|
|
||||||
|
|
||||||
def getorder(self, uuid):
|
|
||||||
return self.query('getorder', {'uuid': uuid})
|
|
||||||
|
|
||||||
def getorderhistory(self, market, count):
|
|
||||||
return self.query('getorderhistory',
|
|
||||||
{'market': market, 'count': count})
|
|
||||||
|
|
||||||
def getwithdrawalhistory(self, currency, count):
|
|
||||||
return self.query('getwithdrawalhistory',
|
|
||||||
{'currency': currency, 'count': count})
|
|
||||||
|
|
||||||
def getdeposithistory(self, currency, count):
|
|
||||||
return self.query('getdeposithistory',
|
|
||||||
{'currency': currency, 'count': count})
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from catalyst.data.bundles import register
|
|
||||||
from catalyst.exchange.exchange_bundle import exchange_bundle
|
|
||||||
|
|
||||||
symbols = (
|
|
||||||
'neo_btc',
|
|
||||||
)
|
|
||||||
register('exchange_bitfinex', exchange_bundle('bitfinex', symbols))
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
import calendar
|
|
||||||
import os
|
|
||||||
import tarfile
|
|
||||||
from datetime import timedelta, datetime, date
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
from catalyst.data.bundles import from_bundle_ingest_dirname
|
|
||||||
from catalyst.data.bundles.core import download_without_progress
|
|
||||||
from catalyst.exchange.exchange_errors import NoDataAvailableOnExchange
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_bundles_folder
|
|
||||||
from catalyst.utils.deprecate import deprecated
|
|
||||||
from catalyst.utils.paths import data_path
|
|
||||||
|
|
||||||
EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
|
|
||||||
API_URL = 'http://data.enigma.co/api/v1'
|
|
||||||
|
|
||||||
|
|
||||||
def get_date_from_ms(ms):
|
|
||||||
return datetime.fromtimestamp(ms / 1000.0)
|
|
||||||
|
|
||||||
|
|
||||||
def get_seconds_from_date(date):
|
|
||||||
epoch = datetime.utcfromtimestamp(0)
|
|
||||||
epoch = epoch.replace(tzinfo=pytz.UTC)
|
|
||||||
|
|
||||||
return int((date - epoch).total_seconds())
|
|
||||||
|
|
||||||
|
|
||||||
def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
|
||||||
"""
|
|
||||||
Download and extract a bcolz bundle.
|
|
||||||
|
|
||||||
:param exchange_name:
|
|
||||||
:param symbol:
|
|
||||||
:param data_frequency:
|
|
||||||
:param period:
|
|
||||||
:return:
|
|
||||||
|
|
||||||
Note:
|
|
||||||
Filename: bitfinex-daily-neo_eth-2017-10.tar.gz
|
|
||||||
"""
|
|
||||||
|
|
||||||
root = get_exchange_bundles_folder(exchange_name)
|
|
||||||
name = '{exchange}-{frequency}-{symbol}-{period}'.format(
|
|
||||||
exchange=exchange_name,
|
|
||||||
frequency=data_frequency,
|
|
||||||
symbol=symbol,
|
|
||||||
period=period
|
|
||||||
)
|
|
||||||
path = os.path.join(root, name)
|
|
||||||
|
|
||||||
if not os.path.isdir(path):
|
|
||||||
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
|
||||||
'exchange-{exchange}/{name}.tar.gz'.format(
|
|
||||||
exchange=exchange_name,
|
|
||||||
name=name
|
|
||||||
)
|
|
||||||
|
|
||||||
bytes = download_without_progress(url)
|
|
||||||
with tarfile.open('r', fileobj=bytes) as tar:
|
|
||||||
tar.extractall(path)
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def get_delta(periods, data_frequency):
|
|
||||||
return timedelta(minutes=periods) \
|
|
||||||
if data_frequency == 'minute' else timedelta(days=periods)
|
|
||||||
|
|
||||||
|
|
||||||
def get_periods_range(start_dt, end_dt, data_frequency):
|
|
||||||
freq = 'T' if data_frequency == 'minute' else 'D'
|
|
||||||
|
|
||||||
return pd.date_range(start_dt, end_dt, freq=freq)
|
|
||||||
|
|
||||||
|
|
||||||
def get_periods(start_dt, end_dt, data_frequency):
|
|
||||||
delta = end_dt - start_dt
|
|
||||||
|
|
||||||
if data_frequency == 'minute':
|
|
||||||
delta_periods = delta.total_seconds() / 60
|
|
||||||
|
|
||||||
elif data_frequency == 'daily':
|
|
||||||
delta_periods = delta.total_seconds() / 60 / 60 / 24
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError('frequency not supported')
|
|
||||||
|
|
||||||
return int(delta_periods)
|
|
||||||
|
|
||||||
|
|
||||||
def get_start_dt(end_dt, bar_count, data_frequency):
|
|
||||||
periods = bar_count
|
|
||||||
if periods > 1:
|
|
||||||
delta = get_delta(periods, data_frequency)
|
|
||||||
start_dt = end_dt - delta
|
|
||||||
else:
|
|
||||||
start_dt = end_dt
|
|
||||||
|
|
||||||
return start_dt
|
|
||||||
|
|
||||||
|
|
||||||
def get_adj_dates(start, end, assets, data_frequency):
|
|
||||||
"""
|
|
||||||
Contains a date range to the trading availability of the specified pairs.
|
|
||||||
|
|
||||||
:param start:
|
|
||||||
:param end:
|
|
||||||
:param assets:
|
|
||||||
:param data_frequency:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
earliest_trade = None
|
|
||||||
last_entry = None
|
|
||||||
for asset in assets:
|
|
||||||
if earliest_trade is None or earliest_trade > asset.start_date:
|
|
||||||
earliest_trade = asset.start_date
|
|
||||||
|
|
||||||
end_asset = asset.end_minute if data_frequency == 'minute' else \
|
|
||||||
asset.end_daily
|
|
||||||
if end_asset is not None and \
|
|
||||||
(last_entry is None or end_asset > last_entry):
|
|
||||||
last_entry = end_asset
|
|
||||||
|
|
||||||
if start is None or earliest_trade > start:
|
|
||||||
start = earliest_trade
|
|
||||||
|
|
||||||
if end is None or (last_entry is not None and end > last_entry):
|
|
||||||
end = last_entry
|
|
||||||
|
|
||||||
if end is None or start >= end:
|
|
||||||
raise NoDataAvailableOnExchange(
|
|
||||||
exchange=asset.exchange.title(),
|
|
||||||
symbol=[asset.symbol.encode('utf-8')],
|
|
||||||
data_frequency=data_frequency,
|
|
||||||
)
|
|
||||||
|
|
||||||
return start, end
|
|
||||||
|
|
||||||
|
|
||||||
def get_month_start_end(dt):
|
|
||||||
"""
|
|
||||||
Returns the first and last day of the month for the specified date.
|
|
||||||
|
|
||||||
:param dt:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
month_range = calendar.monthrange(dt.year, dt.month)
|
|
||||||
month_start = pd.to_datetime(datetime(
|
|
||||||
dt.year, dt.month, 1, 0, 0, 0, 0
|
|
||||||
), utc=True)
|
|
||||||
|
|
||||||
month_end = pd.to_datetime(datetime(
|
|
||||||
dt.year, dt.month, month_range[1], 23, 59, 0, 0
|
|
||||||
), utc=True)
|
|
||||||
|
|
||||||
return month_start, month_end
|
|
||||||
|
|
||||||
|
|
||||||
def get_year_start_end(dt):
|
|
||||||
"""
|
|
||||||
Returns the first and last day of the year for the specified date.
|
|
||||||
|
|
||||||
:param dt:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
year_start = pd.to_datetime(date(dt.year, 1, 1), utc=True)
|
|
||||||
year_end = pd.to_datetime(date(dt.year, 12, 31), utc=True)
|
|
||||||
|
|
||||||
return year_start, year_end
|
|
||||||
|
|
||||||
|
|
||||||
def get_df_from_arrays(arrays, periods):
|
|
||||||
ohlcv = dict()
|
|
||||||
for index, field in enumerate(
|
|
||||||
['open', 'high', 'low', 'close', 'volume']):
|
|
||||||
ohlcv[field] = arrays[index].flatten()
|
|
||||||
|
|
||||||
df = pd.DataFrame(
|
|
||||||
data=ohlcv,
|
|
||||||
index=periods
|
|
||||||
)
|
|
||||||
return df
|
|
||||||
|
|
||||||
|
|
||||||
def range_in_bundle(asset, start_dt, end_dt, reader):
|
|
||||||
"""
|
|
||||||
Evaluate whether price data of an asset is included has been ingested in
|
|
||||||
the exchange bundle for the given date range.
|
|
||||||
|
|
||||||
:param asset:
|
|
||||||
:param start_dt:
|
|
||||||
:param end_dt:
|
|
||||||
:param reader:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
has_data = True
|
|
||||||
if has_data and reader is not None:
|
|
||||||
try:
|
|
||||||
start_close = \
|
|
||||||
reader.get_value(asset.sid, start_dt, 'close')
|
|
||||||
|
|
||||||
if np.isnan(start_close):
|
|
||||||
has_data = False
|
|
||||||
|
|
||||||
else:
|
|
||||||
end_close = reader.get_value(asset.sid, end_dt, 'close')
|
|
||||||
|
|
||||||
if np.isnan(end_close):
|
|
||||||
has_data = False
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
has_data = False
|
|
||||||
|
|
||||||
else:
|
|
||||||
has_data = False
|
|
||||||
|
|
||||||
return has_data
|
|
||||||
|
|
||||||
|
|
||||||
@deprecated
|
|
||||||
def find_most_recent_time(bundle_name):
|
|
||||||
"""
|
|
||||||
Find most recent "time folder" for a given bundle.
|
|
||||||
|
|
||||||
:param bundle_name:
|
|
||||||
The name of the targeted bundle.
|
|
||||||
|
|
||||||
:return folder:
|
|
||||||
The name of the time folder.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
bundle_folders = os.listdir(
|
|
||||||
data_path([bundle_name]),
|
|
||||||
)
|
|
||||||
except OSError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
most_recent_bundle = dict()
|
|
||||||
for folder in bundle_folders:
|
|
||||||
date = from_bundle_ingest_dirname(folder)
|
|
||||||
if not most_recent_bundle or date > \
|
|
||||||
most_recent_bundle[most_recent_bundle.keys()[0]]:
|
|
||||||
most_recent_bundle = dict()
|
|
||||||
most_recent_bundle[folder] = date
|
|
||||||
|
|
||||||
if most_recent_bundle:
|
|
||||||
return most_recent_bundle.keys()[0]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,345 +0,0 @@
|
|||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# 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 abc
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.data.data_portal import DataPortal
|
|
||||||
from catalyst.exchange.bundle_utils import get_start_dt
|
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
|
||||||
from catalyst.exchange.exchange_errors import (
|
|
||||||
ExchangeRequestError,
|
|
||||||
ExchangeBarDataError,
|
|
||||||
PricingDataBeforeTradingError,
|
|
||||||
PricingDataNotLoadedError, InvalidHistoryFrequencyError,
|
|
||||||
BundleNotFoundError)
|
|
||||||
|
|
||||||
log = Logger('DataPortalExchange')
|
|
||||||
|
|
||||||
|
|
||||||
class DataPortalExchangeBase(DataPortal):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
|
|
||||||
self.exchanges = kwargs.pop('exchanges', None)
|
|
||||||
# TODO: put somewhere accessible by each algo
|
|
||||||
self.retry_get_history_window = 5
|
|
||||||
self.retry_get_spot_value = 5
|
|
||||||
self.retry_delay = 5
|
|
||||||
|
|
||||||
super(DataPortalExchangeBase, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def _get_history_window(self,
|
|
||||||
assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill=True,
|
|
||||||
attempt_index=0):
|
|
||||||
try:
|
|
||||||
exchange_assets = dict()
|
|
||||||
for asset in assets:
|
|
||||||
if asset.exchange not in exchange_assets:
|
|
||||||
exchange_assets[asset.exchange] = list()
|
|
||||||
|
|
||||||
exchange_assets[asset.exchange].append(asset)
|
|
||||||
|
|
||||||
if len(exchange_assets) > 1:
|
|
||||||
df_list = []
|
|
||||||
for exchange_name in exchange_assets:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
assets = exchange_assets[exchange_name]
|
|
||||||
|
|
||||||
df_exchange = self.get_exchange_history_window(
|
|
||||||
exchange,
|
|
||||||
assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill)
|
|
||||||
|
|
||||||
df_list.append(df_exchange)
|
|
||||||
|
|
||||||
# Merging the values values of each exchange
|
|
||||||
return pd.concat(df_list)
|
|
||||||
|
|
||||||
else:
|
|
||||||
exchange = self.exchanges[exchange_assets.keys()[0]]
|
|
||||||
return self.get_exchange_history_window(
|
|
||||||
exchange,
|
|
||||||
assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill)
|
|
||||||
|
|
||||||
except ExchangeRequestError as e:
|
|
||||||
log.warn(
|
|
||||||
'get history attempt {}: {}'.format(attempt_index, e)
|
|
||||||
)
|
|
||||||
if attempt_index < self.retry_get_history_window:
|
|
||||||
sleep(self.retry_delay)
|
|
||||||
return self._get_history_window(assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill,
|
|
||||||
attempt_index + 1)
|
|
||||||
else:
|
|
||||||
raise ExchangeBarDataError(
|
|
||||||
data_type='history',
|
|
||||||
attempts=attempt_index,
|
|
||||||
error=e
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_history_window(self,
|
|
||||||
assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency=None,
|
|
||||||
ffill=True):
|
|
||||||
|
|
||||||
if field == 'price':
|
|
||||||
field = 'close'
|
|
||||||
|
|
||||||
return self._get_history_window(assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_exchange_history_window(self,
|
|
||||||
exchange,
|
|
||||||
assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill=True):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _get_spot_value(self, assets, field, dt, data_frequency,
|
|
||||||
attempt_index=0):
|
|
||||||
try:
|
|
||||||
if isinstance(assets, TradingPair):
|
|
||||||
exchange = self.exchanges[assets.exchange]
|
|
||||||
spot_values = self.get_exchange_spot_value(
|
|
||||||
exchange, [assets], field, dt, data_frequency)
|
|
||||||
|
|
||||||
if not spot_values:
|
|
||||||
return np.nan
|
|
||||||
|
|
||||||
return spot_values[0]
|
|
||||||
|
|
||||||
else:
|
|
||||||
exchange_assets = dict()
|
|
||||||
for asset in assets:
|
|
||||||
if asset.exchange not in exchange_assets:
|
|
||||||
exchange_assets[asset.exchange] = list()
|
|
||||||
|
|
||||||
exchange_assets[asset.exchange].append(asset)
|
|
||||||
|
|
||||||
if len(exchange_assets.keys()) == 1:
|
|
||||||
exchange = self.exchanges[exchange_assets.keys()[0]]
|
|
||||||
return self.get_exchange_spot_value(
|
|
||||||
exchange, assets, field, dt, data_frequency)
|
|
||||||
|
|
||||||
else:
|
|
||||||
spot_values = []
|
|
||||||
for exchange_name in exchange_assets:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
assets = exchange_assets[exchange_name]
|
|
||||||
exchange_spot_values = self.get_exchange_spot_value(
|
|
||||||
exchange,
|
|
||||||
assets,
|
|
||||||
field,
|
|
||||||
dt,
|
|
||||||
data_frequency
|
|
||||||
)
|
|
||||||
if len(assets) == 1:
|
|
||||||
spot_values.append(exchange_spot_values)
|
|
||||||
else:
|
|
||||||
spot_values += exchange_spot_values
|
|
||||||
|
|
||||||
return spot_values
|
|
||||||
|
|
||||||
except ExchangeRequestError as e:
|
|
||||||
log.warn(
|
|
||||||
'get spot value attempt {}: {}'.format(attempt_index, e)
|
|
||||||
)
|
|
||||||
if attempt_index < self.retry_get_spot_value:
|
|
||||||
sleep(self.retry_delay)
|
|
||||||
return self._get_spot_value(assets, field, dt, data_frequency,
|
|
||||||
attempt_index + 1)
|
|
||||||
else:
|
|
||||||
raise ExchangeBarDataError(
|
|
||||||
data_type='spot',
|
|
||||||
attempts=attempt_index,
|
|
||||||
error=e
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_spot_value(self, assets, field, dt, data_frequency):
|
|
||||||
if field == 'price':
|
|
||||||
field = 'close'
|
|
||||||
|
|
||||||
return self._get_spot_value(assets, field, dt, data_frequency)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_exchange_spot_value(self, exchange, assets, field, dt,
|
|
||||||
data_frequency):
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_adjusted_value(self, asset, field, dt,
|
|
||||||
perspective_dt,
|
|
||||||
data_frequency,
|
|
||||||
spot_value=None):
|
|
||||||
# TODO: does this pertain to cryptocurrencies?
|
|
||||||
log.warn('get_adjusted_value is not implemented yet!')
|
|
||||||
return spot_value
|
|
||||||
|
|
||||||
|
|
||||||
class DataPortalExchangeLive(DataPortalExchangeBase):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(DataPortalExchangeLive, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def get_exchange_history_window(self,
|
|
||||||
exchange,
|
|
||||||
assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill=True):
|
|
||||||
df = exchange.get_history_window(
|
|
||||||
assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill)
|
|
||||||
return df
|
|
||||||
|
|
||||||
def get_exchange_spot_value(self, exchange, assets, field, dt,
|
|
||||||
data_frequency):
|
|
||||||
exchange_spot_values = exchange.get_spot_value(
|
|
||||||
assets, field, dt, data_frequency)
|
|
||||||
|
|
||||||
return exchange_spot_values
|
|
||||||
|
|
||||||
|
|
||||||
class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(DataPortalExchangeBacktest, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.exchange_bundles = dict()
|
|
||||||
|
|
||||||
self.history_loaders = dict()
|
|
||||||
self.minute_history_loaders = dict()
|
|
||||||
|
|
||||||
for exchange_name in self.exchanges:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
self.exchange_bundles[exchange_name] = ExchangeBundle(exchange)
|
|
||||||
|
|
||||||
def _get_first_trading_day(self, assets):
|
|
||||||
first_date = None
|
|
||||||
for asset in assets:
|
|
||||||
if first_date is None or asset.start_date > first_date:
|
|
||||||
first_date = asset.start_date
|
|
||||||
return first_date
|
|
||||||
|
|
||||||
def get_exchange_history_window(self,
|
|
||||||
exchange,
|
|
||||||
assets,
|
|
||||||
end_dt,
|
|
||||||
bar_count,
|
|
||||||
frequency,
|
|
||||||
field,
|
|
||||||
data_frequency,
|
|
||||||
ffill=True):
|
|
||||||
"""
|
|
||||||
Fetching price history window from the exchange bundle.
|
|
||||||
|
|
||||||
Using a try... except approach to minimize reads most of the time,
|
|
||||||
when the data exists.
|
|
||||||
|
|
||||||
:param exchange:
|
|
||||||
:param assets:
|
|
||||||
:param end_dt:
|
|
||||||
:param bar_count:
|
|
||||||
:param frequency:
|
|
||||||
:param field:
|
|
||||||
:param data_frequency:
|
|
||||||
:param ffill:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
|
|
||||||
bundle = self.exchange_bundles[exchange.name]
|
|
||||||
series = bundle.get_history_window_series_and_load(
|
|
||||||
assets=assets,
|
|
||||||
end_dt=end_dt,
|
|
||||||
bar_count=bar_count,
|
|
||||||
field=field,
|
|
||||||
data_frequency=data_frequency
|
|
||||||
)
|
|
||||||
return pd.DataFrame(series)
|
|
||||||
|
|
||||||
def get_exchange_spot_value(self, exchange, assets, field, dt,
|
|
||||||
data_frequency):
|
|
||||||
bundle = self.exchange_bundles[exchange.name]
|
|
||||||
|
|
||||||
if data_frequency == 'daily':
|
|
||||||
dt = dt.floor('1D')
|
|
||||||
else:
|
|
||||||
dt = dt.floor('1 min')
|
|
||||||
|
|
||||||
try:
|
|
||||||
return bundle.get_spot_values(assets, field, dt, data_frequency)
|
|
||||||
|
|
||||||
except PricingDataNotLoadedError:
|
|
||||||
log.info(
|
|
||||||
'pricing data for {symbol} not found on {dt}'
|
|
||||||
', updating the bundles.'.format(
|
|
||||||
symbol=[asset.symbol for asset in assets],
|
|
||||||
dt=dt
|
|
||||||
)
|
|
||||||
)
|
|
||||||
bundle.ingest_assets(
|
|
||||||
assets=assets,
|
|
||||||
start_dt=self._first_trading_day,
|
|
||||||
end_dt=self._last_available_session,
|
|
||||||
data_frequency=data_frequency,
|
|
||||||
show_progress=True
|
|
||||||
)
|
|
||||||
return bundle.get_spot_values(
|
|
||||||
assets, field, dt, data_frequency, True
|
|
||||||
)
|
|
||||||
+589
-331
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
from catalyst.exchange.utils.factory import find_exchanges
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
log = Logger('ExchangeAssetFinder', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeAssetFinder(object):
|
||||||
|
def __init__(self, exchanges):
|
||||||
|
self.exchanges = exchanges
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sids(self):
|
||||||
|
"""
|
||||||
|
This seems to be used to pre-fetch assets.
|
||||||
|
I don't think that we need this for live-trading.
|
||||||
|
Leaving the list empty.
|
||||||
|
"""
|
||||||
|
all_sids = []
|
||||||
|
for exchange_name in self.exchanges:
|
||||||
|
# This is what initializes each exchanges at the beginning
|
||||||
|
# of an algo
|
||||||
|
exchange = self.exchanges[exchange_name]
|
||||||
|
exchange.init()
|
||||||
|
|
||||||
|
all_sids += [asset.sid for asset in exchange.assets]
|
||||||
|
|
||||||
|
sids = list(set(all_sids))
|
||||||
|
return sids
|
||||||
|
|
||||||
|
def retrieve_asset(self, sid, default_none=False):
|
||||||
|
"""
|
||||||
|
Retrieve the first Asset found for a given sid.
|
||||||
|
"""
|
||||||
|
asset = None
|
||||||
|
for exchange_name in self.exchanges:
|
||||||
|
if asset is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
exchange = self.exchanges[exchange_name]
|
||||||
|
assets = [asset for asset in exchange.assets if asset.sid == sid]
|
||||||
|
if assets:
|
||||||
|
asset = assets[0]
|
||||||
|
|
||||||
|
return asset
|
||||||
|
|
||||||
|
def retrieve_all(self, sids, default_none=False):
|
||||||
|
"""
|
||||||
|
Retrieve all assets in `sids`.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
sids : iterable of int
|
||||||
|
Assets to retrieve.
|
||||||
|
default_none : bool
|
||||||
|
If True, return None for failed lookups.
|
||||||
|
If False, raise `SidsNotFound`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
assets : list[Asset or None]
|
||||||
|
A list of the same length as `sids` containing Assets (or Nones)
|
||||||
|
corresponding to the requested sids.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
SidsNotFound
|
||||||
|
When a requested sid is not found and default_none=False.
|
||||||
|
"""
|
||||||
|
assets = []
|
||||||
|
for exchange_name in self.exchanges:
|
||||||
|
exchange = self.exchanges[exchange_name]
|
||||||
|
xas = [asset for asset in exchange.assets if asset.sid in sids]
|
||||||
|
assets += xas
|
||||||
|
|
||||||
|
return assets
|
||||||
|
|
||||||
|
def lookup_symbol(self, symbol, exchange, data_frequency=None,
|
||||||
|
as_of_date=None, fuzzy=False):
|
||||||
|
"""Lookup an asset by symbol.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
symbol : str
|
||||||
|
The ticker symbol to resolve.
|
||||||
|
as_of_date : datetime or None
|
||||||
|
Look up the last owner of this symbol as of this datetime.
|
||||||
|
If ``as_of_date`` is None, then this can only resolve the equity
|
||||||
|
if exactly one equity has ever owned the ticker.
|
||||||
|
fuzzy : bool, optional
|
||||||
|
Should fuzzy symbol matching be used? Fuzzy symbol matching
|
||||||
|
attempts to resolve differences in representations for
|
||||||
|
shareclasses. For example, some people may represent the ``A``
|
||||||
|
shareclass of ``BRK`` as ``BRK.A``, where others could write
|
||||||
|
``BRK_A``.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
equity : Asset
|
||||||
|
The equity that held ``symbol`` on the given ``as_of_date``, or the
|
||||||
|
only equity to hold ``symbol`` if ``as_of_date`` is None.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
SymbolNotFound
|
||||||
|
Raised when no equity has ever held the given symbol.
|
||||||
|
MultipleSymbolsFound
|
||||||
|
Raised when no ``as_of_date`` is given and more than one equity
|
||||||
|
has held ``symbol``. This is also raised when ``fuzzy=True`` and
|
||||||
|
there are multiple candidates for the given ``symbol`` on the
|
||||||
|
``as_of_date``.
|
||||||
|
"""
|
||||||
|
log.debug('looking up symbol: {} {}'.format(symbol, exchange.name))
|
||||||
|
|
||||||
|
return exchange.get_asset(symbol, data_frequency)
|
||||||
|
|
||||||
|
def lifetimes(self, dates, include_start_date):
|
||||||
|
"""
|
||||||
|
Compute a DataFrame representing asset lifetimes for the specified date
|
||||||
|
range.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dates : pd.DatetimeIndex
|
||||||
|
The dates for which to compute lifetimes.
|
||||||
|
include_start_date : bool
|
||||||
|
Whether or not to count the asset as alive on its start_date.
|
||||||
|
|
||||||
|
This is useful in a backtesting context where `lifetimes` is being
|
||||||
|
used to signify "do I have data for this asset as of the morning of
|
||||||
|
this date?" For many financial metrics, (e.g. daily close), data
|
||||||
|
isn't available for an asset until the end of the asset's first
|
||||||
|
day.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
lifetimes : pd.DataFrame
|
||||||
|
A frame of dtype bool with `dates` as index and an Int64Index of
|
||||||
|
assets as columns. The value at `lifetimes.loc[date, asset]` will
|
||||||
|
be True iff `asset` existed on `date`. If `include_start_date` is
|
||||||
|
False, then lifetimes.loc[date, asset] will be false when date ==
|
||||||
|
asset.start_date.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
numpy.putmask
|
||||||
|
catalyst.pipeline.engine.SimplePipelineEngine._compute_root_mask
|
||||||
|
"""
|
||||||
|
exchanges = find_exchanges(features=['minuteBundle'])
|
||||||
|
if not exchanges:
|
||||||
|
raise ValueError('exchange with minute bundles not found')
|
||||||
|
|
||||||
|
# TODO: find a way to support multiple exchanges
|
||||||
|
exchange = exchanges[0]
|
||||||
|
# Using a single exchange for now because are not unique for the
|
||||||
|
# same asset in different exchanges. I'd like to avoid binding
|
||||||
|
# pipeline to a single exchange.
|
||||||
|
exchange.init()
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for dt in dates:
|
||||||
|
exists = []
|
||||||
|
|
||||||
|
for asset in exchange.assets:
|
||||||
|
if include_start_date:
|
||||||
|
condition = (asset.start_date <= dt < asset.end_minute)
|
||||||
|
|
||||||
|
else:
|
||||||
|
condition = (asset.start_date < dt < asset.end_minute)
|
||||||
|
|
||||||
|
exists.append(condition)
|
||||||
|
|
||||||
|
data.append(exists)
|
||||||
|
|
||||||
|
sids = [asset.sid for asset in exchange.assets]
|
||||||
|
df = pd.DataFrame(data, index=dates, columns=exchange.assets)
|
||||||
|
|
||||||
|
return df
|
||||||
@@ -3,7 +3,6 @@ import numpy as np
|
|||||||
from catalyst import get_calendar
|
from catalyst import get_calendar
|
||||||
from catalyst.data.minute_bars import BcolzMinuteBarReader, \
|
from catalyst.data.minute_bars import BcolzMinuteBarReader, \
|
||||||
BcolzMinuteBarWriter
|
BcolzMinuteBarWriter
|
||||||
from catalyst.exchange.bundle_utils import get_periods, get_periods_range
|
|
||||||
|
|
||||||
|
|
||||||
class BcolzExchangeBarWriter(BcolzMinuteBarWriter):
|
class BcolzExchangeBarWriter(BcolzMinuteBarWriter):
|
||||||
@@ -17,7 +16,7 @@ class BcolzExchangeBarWriter(BcolzMinuteBarWriter):
|
|||||||
end_session = end_session.floor('1d')
|
end_session = end_session.floor('1d')
|
||||||
|
|
||||||
minutes_per_day = 1440 if self._data_frequency == 'minute' else 1
|
minutes_per_day = 1440 if self._data_frequency == 'minute' else 1
|
||||||
default_ohlc_ratio = kwargs.pop('default_ohlc_ratio', 1000000)
|
default_ohlc_ratio = kwargs.pop('default_ohlc_ratio', 100000000)
|
||||||
calendar = get_calendar('OPEN')
|
calendar = get_calendar('OPEN')
|
||||||
|
|
||||||
super(BcolzExchangeBarWriter, self) \
|
super(BcolzExchangeBarWriter, self) \
|
||||||
@@ -40,17 +39,25 @@ class BcolzExchangeBarReader(BcolzMinuteBarReader):
|
|||||||
return self._data_frequency
|
return self._data_frequency
|
||||||
|
|
||||||
def load_raw_arrays(self, fields, start_dt, end_dt, sids):
|
def load_raw_arrays(self, fields, start_dt, end_dt, sids):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
fields : list of str
|
||||||
|
'open', 'high', 'low', 'close', or 'volume'
|
||||||
|
start_dt: Timestamp
|
||||||
|
Beginning of the window range.
|
||||||
|
end_dt: Timestamp
|
||||||
|
End of the window range.
|
||||||
|
sids : list of int
|
||||||
|
The asset identifiers in the window.
|
||||||
|
|
||||||
# if self._data_frequency == 'minute':
|
Returns
|
||||||
# return super(BcolzExchangeBarReader, self) \
|
-------
|
||||||
# .load_raw_arrays(fields, start_dt, end_dt, sids)
|
list of np.ndarray
|
||||||
#
|
A list with an entry per field of ndarrays with shape
|
||||||
# else:
|
(minutes in range, sids) with a dtype of float64, containing the
|
||||||
# return self._load_daily_raw_arrays(fields, start_dt, end_dt, sids)
|
values for the respective field over start and end dt range.
|
||||||
|
"""
|
||||||
return self._load_raw_arrays(fields, start_dt, end_dt, sids)
|
|
||||||
|
|
||||||
def _load_raw_arrays(self, fields, start_dt, end_dt, sids):
|
|
||||||
start_idx = self._find_position_of_minute(start_dt)
|
start_idx = self._find_position_of_minute(start_dt)
|
||||||
end_idx = self._find_position_of_minute(end_dt)
|
end_idx = self._find_position_of_minute(end_dt)
|
||||||
|
|
||||||
@@ -80,8 +87,9 @@ class BcolzExchangeBarReader(BcolzMinuteBarReader):
|
|||||||
if mask is None:
|
if mask is None:
|
||||||
mask = a != 0
|
mask = a != 0
|
||||||
|
|
||||||
|
inverse_ratio = self._ohlc_ratio_inverse_for_sid(sid)
|
||||||
out[:len(mask), i][mask] = (
|
out[:len(mask), i][mask] = (
|
||||||
a[mask] * self._ohlc_ratio_inverse_for_sid(sid)
|
a[mask] * inverse_ratio
|
||||||
)
|
)
|
||||||
|
|
||||||
if field in fields:
|
if field in fields:
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
from catalyst.assets._assets import TradingPair
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
from redo import retry
|
||||||
|
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
from catalyst.exchange.exchange_errors import ExchangeRequestError
|
||||||
from catalyst.finance.blotter import Blotter
|
from catalyst.finance.blotter import Blotter
|
||||||
from catalyst.finance.commission import CommissionModel
|
from catalyst.finance.commission import CommissionModel
|
||||||
|
from catalyst.finance.order import ORDER_STATUS
|
||||||
from catalyst.finance.slippage import SlippageModel
|
from catalyst.finance.slippage import SlippageModel
|
||||||
from catalyst.finance.transaction import Transaction
|
from catalyst.finance.transaction import create_transaction, Transaction
|
||||||
|
from catalyst.utils.input_validation import expect_types
|
||||||
|
|
||||||
log = Logger('exchange_blotter')
|
log = Logger('exchange_blotter', level=LOG_LEVEL)
|
||||||
|
|
||||||
# It seems like we need to accept greater slippage risk in cryptos
|
|
||||||
# Orders won't often close at Equity levels.
|
|
||||||
# TODO: consider adjusting dynamically based on trading pair
|
|
||||||
DEFAULT_SLIPPAGE_SPREAD = 0.02
|
|
||||||
DEFAULT_MAKER_FEE = 0.001
|
|
||||||
DEFAULT_TAKER_FEE = 0.002
|
|
||||||
|
|
||||||
|
|
||||||
class TradingPairFeeSchedule(CommissionModel):
|
class TradingPairFeeSchedule(CommissionModel):
|
||||||
@@ -22,40 +22,55 @@ class TradingPairFeeSchedule(CommissionModel):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
fee : float, optional
|
maker : float, optional
|
||||||
The percentage fee.
|
The percentage maker fee.
|
||||||
|
|
||||||
|
taker: float, optional
|
||||||
|
The percentage taker fee.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, maker=None, taker=None):
|
||||||
maker_fee=DEFAULT_MAKER_FEE,
|
self.maker = maker
|
||||||
taker_fee=DEFAULT_TAKER_FEE):
|
self.taker = taker
|
||||||
self.maker_fee = maker_fee
|
|
||||||
self.taker_fee = taker_fee
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
'{class_name}(maker_fee={maker_fee}, '
|
'{class_name}(maker={maker}, '
|
||||||
'taker_fee={taker_fee})'.format(
|
'taker={taker})'.format(
|
||||||
class_name=self.__class__.__name__,
|
class_name=self.__class__.__name__,
|
||||||
maker_fee=self.maker_fee,
|
maker=self.maker,
|
||||||
taker_fee=self.taker_fee,
|
taker=self.taker,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_maker_taker(self, asset):
|
||||||
|
maker = self.maker if self.maker is not None else asset.maker
|
||||||
|
taker = self.taker if self.taker is not None else asset.taker
|
||||||
|
return maker, taker
|
||||||
|
|
||||||
def calculate(self, order, transaction):
|
def calculate(self, order, transaction):
|
||||||
"""
|
"""
|
||||||
Calculate the final fee based on the order parameters.
|
Calculate the final fee based on the order parameters.
|
||||||
|
|
||||||
:param order:
|
:param order: Order
|
||||||
:param transaction:
|
:param transaction: Transaction
|
||||||
|
|
||||||
:return float:
|
:return float:
|
||||||
The total commission.
|
The total commission.
|
||||||
"""
|
"""
|
||||||
cost = abs(transaction.amount) * transaction.price
|
cost = abs(transaction.amount) * transaction.price
|
||||||
|
|
||||||
# Assuming just the taker fee for now
|
asset = order.asset
|
||||||
fee = cost * self.taker_fee
|
maker, taker = self.get_maker_taker(asset)
|
||||||
|
|
||||||
|
multiplier = taker
|
||||||
|
if order.limit is not None:
|
||||||
|
multiplier = maker \
|
||||||
|
if ((order.amount > 0 and order.limit < transaction.price)
|
||||||
|
or (order.amount < 0 and order.limit > transaction.price)) \
|
||||||
|
and order.limit_reached else taker
|
||||||
|
|
||||||
|
fee = cost * multiplier
|
||||||
return fee
|
return fee
|
||||||
|
|
||||||
|
|
||||||
@@ -69,7 +84,7 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
spread / 2 will be added to buys and subtracted from sells.
|
spread / 2 will be added to buys and subtracted from sells.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, spread=DEFAULT_SLIPPAGE_SPREAD):
|
def __init__(self, spread=0.0001):
|
||||||
super(TradingPairFixedSlippage, self).__init__()
|
super(TradingPairFixedSlippage, self).__init__()
|
||||||
self.spread = spread
|
self.spread = spread
|
||||||
|
|
||||||
@@ -80,7 +95,6 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
|
|
||||||
def simulate(self, data, asset, orders_for_asset):
|
def simulate(self, data, asset, orders_for_asset):
|
||||||
self._volume_for_bar = 0
|
self._volume_for_bar = 0
|
||||||
|
|
||||||
price = data.current(asset, 'close')
|
price = data.current(asset, 'close')
|
||||||
|
|
||||||
dt = data.current_dt
|
dt = data.current_dt
|
||||||
@@ -90,22 +104,20 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
|
|
||||||
order.check_triggers(price, dt)
|
order.check_triggers(price, dt)
|
||||||
if not order.triggered:
|
if not order.triggered:
|
||||||
log.debug('order has not reached the trigger at current '
|
log.info(
|
||||||
'price {}'.format(price))
|
'order has not reached the trigger at current '
|
||||||
|
'price {}'.format(price)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
execution_price, execution_volume = self.process_order(data, order)
|
execution_price, execution_volume = self.process_order(data, order)
|
||||||
|
if execution_price is not None:
|
||||||
|
transaction = create_transaction(
|
||||||
|
order, dt, execution_price, execution_volume
|
||||||
|
)
|
||||||
|
|
||||||
transaction = Transaction(
|
self._volume_for_bar += abs(transaction.amount)
|
||||||
asset=order.asset,
|
yield order, transaction
|
||||||
amount=abs(execution_volume),
|
|
||||||
dt=dt,
|
|
||||||
price=execution_price,
|
|
||||||
order_id=order.id
|
|
||||||
)
|
|
||||||
|
|
||||||
self._volume_for_bar += abs(transaction.amount)
|
|
||||||
yield order, transaction
|
|
||||||
|
|
||||||
def process_order(self, data, order):
|
def process_order(self, data, order):
|
||||||
price = data.current(order.asset, 'close')
|
price = data.current(order.asset, 'close')
|
||||||
@@ -124,6 +136,15 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
|
|
||||||
class ExchangeBlotter(Blotter):
|
class ExchangeBlotter(Blotter):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.simulate_orders = kwargs.pop('simulate_orders', False)
|
||||||
|
self.attempts = kwargs.pop('attempts', False)
|
||||||
|
|
||||||
|
self.exchanges = kwargs.pop('exchanges', None)
|
||||||
|
if not self.exchanges:
|
||||||
|
raise ValueError(
|
||||||
|
'ExchangeBlotter must have an `exchanges` attribute.'
|
||||||
|
)
|
||||||
|
|
||||||
super(ExchangeBlotter, self).__init__(*args, **kwargs)
|
super(ExchangeBlotter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Using the equity models for now
|
# Using the equity models for now
|
||||||
@@ -135,3 +156,122 @@ class ExchangeBlotter(Blotter):
|
|||||||
self.commission_models = {
|
self.commission_models = {
|
||||||
TradingPair: TradingPairFeeSchedule()
|
TradingPair: TradingPairFeeSchedule()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def exchange_order(self, asset, amount, style=None):
|
||||||
|
exchange = self.exchanges[asset.exchange]
|
||||||
|
return exchange.order(
|
||||||
|
asset, amount, style
|
||||||
|
)
|
||||||
|
|
||||||
|
@expect_types(asset=TradingPair)
|
||||||
|
def order(self, asset, amount, style, order_id=None):
|
||||||
|
log.debug('ordering {} {}'.format(amount, asset.symbol))
|
||||||
|
if amount == 0:
|
||||||
|
log.warn('skipping 0 amount orders')
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.simulate_orders:
|
||||||
|
return super(ExchangeBlotter, self).order(
|
||||||
|
asset, amount, style, order_id
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
order = retry(
|
||||||
|
action=self.exchange_order,
|
||||||
|
attempts=self.attempts['order_attempts'],
|
||||||
|
sleeptime=self.attempts['retry_sleeptime'],
|
||||||
|
retry_exceptions=(ExchangeRequestError,),
|
||||||
|
cleanup=lambda: log.warn('Ordering again.'),
|
||||||
|
args=(asset, amount, style),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.open_orders[order.asset].append(order)
|
||||||
|
self.orders[order.id] = order
|
||||||
|
self.new_orders.append(order)
|
||||||
|
|
||||||
|
return order.id
|
||||||
|
|
||||||
|
def check_open_orders(self):
|
||||||
|
"""
|
||||||
|
Loop through the list of open orders in the Portfolio object.
|
||||||
|
For each executed order found, create a transaction and apply to the
|
||||||
|
Portfolio.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[Transaction]
|
||||||
|
|
||||||
|
"""
|
||||||
|
for asset in self.open_orders:
|
||||||
|
exchange = self.exchanges[asset.exchange]
|
||||||
|
|
||||||
|
for order in self.open_orders[asset]:
|
||||||
|
log.debug('found open order: {}'.format(order.id))
|
||||||
|
|
||||||
|
transactions = exchange.process_order(order)
|
||||||
|
# This is a temporary measure, we should really update all
|
||||||
|
# trades, not just when the order gets filled. I just think
|
||||||
|
# that this is safer until we have a robust way to track
|
||||||
|
# the trades already processed by the algo. We can't loose
|
||||||
|
# them if the algo shuts down.
|
||||||
|
if transactions and order.status == ORDER_STATUS.FILLED:
|
||||||
|
avg_price = np.average(
|
||||||
|
a=[t.price for t in transactions],
|
||||||
|
weights=[t.amount for t in transactions],
|
||||||
|
)
|
||||||
|
ostatus = 'filled' if order.open_amount == 0 else 'partial'
|
||||||
|
log.info(
|
||||||
|
'{} order {} / {}: {}, avg price: {}'.format(
|
||||||
|
ostatus,
|
||||||
|
order.id,
|
||||||
|
asset.symbol,
|
||||||
|
order.filled,
|
||||||
|
avg_price,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for transaction in transactions:
|
||||||
|
yield order, transaction
|
||||||
|
|
||||||
|
elif order.status == ORDER_STATUS.CANCELLED:
|
||||||
|
yield order, None
|
||||||
|
|
||||||
|
else:
|
||||||
|
delta = pd.Timestamp.utcnow() - order.dt
|
||||||
|
log.info(
|
||||||
|
'{exchange} order {order_id} for {symbol} still open '
|
||||||
|
'after {delta}'.format(
|
||||||
|
exchange=exchange.name,
|
||||||
|
order_id=order.id,
|
||||||
|
delta=delta,
|
||||||
|
symbol=order.asset.symbol,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_exchange_transactions(self):
|
||||||
|
closed_orders = []
|
||||||
|
transactions = []
|
||||||
|
commissions = []
|
||||||
|
|
||||||
|
for order, txn in self.check_open_orders():
|
||||||
|
order.dt = txn.dt
|
||||||
|
transactions.append(txn)
|
||||||
|
|
||||||
|
if not order.open:
|
||||||
|
closed_orders.append(order)
|
||||||
|
|
||||||
|
return transactions, commissions, closed_orders
|
||||||
|
|
||||||
|
def get_transactions(self, bar_data):
|
||||||
|
if self.simulate_orders:
|
||||||
|
return super(ExchangeBlotter, self).get_transactions(bar_data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return retry(
|
||||||
|
action=self.get_exchange_transactions,
|
||||||
|
attempts=self.attempts['get_transactions_attempts'],
|
||||||
|
sleeptime=self.attempts['retry_sleeptime'],
|
||||||
|
retry_exceptions=(ExchangeRequestError,),
|
||||||
|
cleanup=lambda: log.warn(
|
||||||
|
'Fetching exchange transactions again.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,373 @@
|
|||||||
|
import abc
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from catalyst.constants import LOG_LEVEL, AUTO_INGEST
|
||||||
|
from catalyst.data.data_portal import DataPortal
|
||||||
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
|
from catalyst.exchange.exchange_errors import (
|
||||||
|
ExchangeRequestError,
|
||||||
|
PricingDataNotLoadedError)
|
||||||
|
from catalyst.exchange.utils.exchange_utils import resample_history_df, \
|
||||||
|
group_assets_by_exchange
|
||||||
|
from catalyst.exchange.utils.datetime_utils import get_frequency, get_start_dt
|
||||||
|
from logbook import Logger
|
||||||
|
from redo import retry
|
||||||
|
|
||||||
|
log = Logger('DataPortalExchange', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
|
class DataPortalExchangeBase(DataPortal):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.attempts = dict(
|
||||||
|
get_spot_value_attempts=5,
|
||||||
|
get_history_window_attempts=5,
|
||||||
|
retry_sleeptime=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
super(DataPortalExchangeBase, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _get_history_window(self,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill=True):
|
||||||
|
exchange_assets = group_assets_by_exchange(assets)
|
||||||
|
if len(exchange_assets) > 1:
|
||||||
|
df_list = []
|
||||||
|
for exchange_name in exchange_assets:
|
||||||
|
assets = exchange_assets[exchange_name]
|
||||||
|
|
||||||
|
df_exchange = self.get_exchange_history_window(
|
||||||
|
exchange_name,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill)
|
||||||
|
|
||||||
|
df_list.append(df_exchange)
|
||||||
|
|
||||||
|
# Merging the values values of each exchange
|
||||||
|
return pd.concat(df_list)
|
||||||
|
|
||||||
|
else:
|
||||||
|
exchange_name = list(exchange_assets.keys())[0]
|
||||||
|
return self.get_exchange_history_window(
|
||||||
|
exchange_name,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill)
|
||||||
|
|
||||||
|
def get_history_window(self,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency=None,
|
||||||
|
ffill=True):
|
||||||
|
|
||||||
|
if field == 'price':
|
||||||
|
field = 'close'
|
||||||
|
|
||||||
|
return retry(
|
||||||
|
action=self._get_history_window,
|
||||||
|
attempts=self.attempts['get_history_window_attempts'],
|
||||||
|
sleeptime=self.attempts['retry_sleeptime'],
|
||||||
|
retry_exceptions=(ExchangeRequestError,),
|
||||||
|
cleanup=lambda: log.warn('fetching history again.'),
|
||||||
|
args=(assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill))
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_exchange_history_window(self,
|
||||||
|
exchange_name,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill=True):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_spot_value(self, assets, field, dt, data_frequency):
|
||||||
|
if isinstance(assets, TradingPair):
|
||||||
|
spot_values = self.get_exchange_spot_value(
|
||||||
|
assets.exchange, [assets], field, dt, data_frequency)
|
||||||
|
|
||||||
|
if not spot_values:
|
||||||
|
return np.nan
|
||||||
|
|
||||||
|
return spot_values[0]
|
||||||
|
|
||||||
|
else:
|
||||||
|
exchange_assets = dict()
|
||||||
|
for asset in assets:
|
||||||
|
if asset.exchange not in exchange_assets:
|
||||||
|
exchange_assets[asset.exchange] = list()
|
||||||
|
|
||||||
|
exchange_assets[asset.exchange].append(asset)
|
||||||
|
|
||||||
|
if len(list(exchange_assets.keys())) == 1:
|
||||||
|
exchange_name = list(exchange_assets.keys())[0]
|
||||||
|
return self.get_exchange_spot_value(
|
||||||
|
exchange_name, assets, field, dt, data_frequency)
|
||||||
|
|
||||||
|
else:
|
||||||
|
spot_values = []
|
||||||
|
for exchange_name in exchange_assets:
|
||||||
|
assets = exchange_assets[exchange_name]
|
||||||
|
exchange_spot_values = self.get_exchange_spot_value(
|
||||||
|
exchange_name,
|
||||||
|
assets,
|
||||||
|
field,
|
||||||
|
dt,
|
||||||
|
data_frequency
|
||||||
|
)
|
||||||
|
if len(assets) == 1:
|
||||||
|
spot_values.append(exchange_spot_values)
|
||||||
|
else:
|
||||||
|
spot_values += exchange_spot_values
|
||||||
|
|
||||||
|
return spot_values
|
||||||
|
|
||||||
|
def get_spot_value(self, assets, field, dt, data_frequency):
|
||||||
|
if field == 'price':
|
||||||
|
field = 'close'
|
||||||
|
|
||||||
|
return retry(
|
||||||
|
action=self._get_spot_value,
|
||||||
|
attempts=self.attempts['get_spot_value_attempts'],
|
||||||
|
sleeptime=self.attempts['retry_sleeptime'],
|
||||||
|
retry_exceptions=(ExchangeRequestError,),
|
||||||
|
cleanup=lambda: log.warn('fetching spot value again.'),
|
||||||
|
args=(assets, field, dt, data_frequency))
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
|
||||||
|
data_frequency):
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_adjusted_value(self, asset, field, dt,
|
||||||
|
perspective_dt,
|
||||||
|
data_frequency,
|
||||||
|
spot_value=None):
|
||||||
|
# TODO: does this pertain to cryptocurrencies?
|
||||||
|
log.warn('get_adjusted_value is not implemented yet!')
|
||||||
|
return spot_value
|
||||||
|
|
||||||
|
|
||||||
|
class DataPortalExchangeLive(DataPortalExchangeBase):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.exchanges = kwargs.pop('exchanges', None)
|
||||||
|
super(DataPortalExchangeLive, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_exchange_history_window(self,
|
||||||
|
exchange_name,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill=True):
|
||||||
|
"""
|
||||||
|
Fetching price history window from the exchange.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: Exchange
|
||||||
|
assets: list[TradingPair]
|
||||||
|
end_dt: datetime
|
||||||
|
bar_count: int
|
||||||
|
frequency: str
|
||||||
|
field: str
|
||||||
|
data_frequency: str
|
||||||
|
ffill: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
|
||||||
|
"""
|
||||||
|
exchange = self.exchanges[exchange_name]
|
||||||
|
|
||||||
|
df = exchange.get_history_window(
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
False)
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_exchange_spot_value(self, exchange_name, assets, field, dt,
|
||||||
|
data_frequency):
|
||||||
|
"""
|
||||||
|
A spot value for the exchange.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
assets: list[TradingPair]
|
||||||
|
field: str
|
||||||
|
dt: datetime
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
|
"""
|
||||||
|
exchange = self.exchanges[exchange_name]
|
||||||
|
exchange_spot_values = exchange.get_spot_value(
|
||||||
|
assets, field, dt, data_frequency)
|
||||||
|
|
||||||
|
return exchange_spot_values
|
||||||
|
|
||||||
|
|
||||||
|
class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.exchange_names = kwargs.pop('exchange_names', None)
|
||||||
|
|
||||||
|
super(DataPortalExchangeBacktest, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.exchange_bundles = dict()
|
||||||
|
self.history_loaders = dict()
|
||||||
|
self.minute_history_loaders = dict()
|
||||||
|
|
||||||
|
for name in self.exchange_names:
|
||||||
|
self.exchange_bundles[name] = ExchangeBundle(name)
|
||||||
|
|
||||||
|
def _get_first_trading_day(self, assets):
|
||||||
|
first_date = None
|
||||||
|
for asset in assets:
|
||||||
|
if first_date is None or asset.start_date > first_date:
|
||||||
|
first_date = asset.start_date
|
||||||
|
return first_date
|
||||||
|
|
||||||
|
def get_exchange_history_window(self,
|
||||||
|
exchange_name,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill=True):
|
||||||
|
"""
|
||||||
|
Fetching price history window from the exchange bundle.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange: Exchange
|
||||||
|
assets: list[TradingPair]
|
||||||
|
end_dt: datetime
|
||||||
|
bar_count: int
|
||||||
|
frequency: str
|
||||||
|
field: str
|
||||||
|
data_frequency: str
|
||||||
|
ffill: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: verify that the exchange supports the timeframe
|
||||||
|
bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
|
||||||
|
|
||||||
|
freq, candle_size, unit, adj_data_frequency = get_frequency(
|
||||||
|
frequency, data_frequency, supported_freqs=['T', 'D']
|
||||||
|
)
|
||||||
|
adj_bar_count = candle_size * bar_count
|
||||||
|
|
||||||
|
if data_frequency == 'minute' and adj_data_frequency == 'daily':
|
||||||
|
end_dt = end_dt.floor('1D')
|
||||||
|
|
||||||
|
series = bundle.get_history_window_series_and_load(
|
||||||
|
assets=assets,
|
||||||
|
end_dt=end_dt,
|
||||||
|
bar_count=adj_bar_count,
|
||||||
|
field=field,
|
||||||
|
data_frequency=adj_data_frequency,
|
||||||
|
algo_end_dt=self._last_available_session,
|
||||||
|
)
|
||||||
|
|
||||||
|
start_dt = get_start_dt(end_dt, adj_bar_count, adj_data_frequency)
|
||||||
|
df = resample_history_df(pd.DataFrame(series), freq, field, start_dt)
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_exchange_spot_value(self,
|
||||||
|
exchange_name,
|
||||||
|
assets,
|
||||||
|
field,
|
||||||
|
dt,
|
||||||
|
data_frequency
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
A spot value for the exchange bundle. Try to ingest data if not in
|
||||||
|
the bundle.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
assets: list[TradingPair]
|
||||||
|
field: str
|
||||||
|
dt: datetime
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
|
"""
|
||||||
|
bundle = self.exchange_bundles[exchange_name]
|
||||||
|
if data_frequency == 'daily':
|
||||||
|
dt = dt.floor('1D')
|
||||||
|
else:
|
||||||
|
dt = dt.floor('1 min')
|
||||||
|
|
||||||
|
if AUTO_INGEST:
|
||||||
|
try:
|
||||||
|
return bundle.get_spot_values(
|
||||||
|
assets, field, dt, data_frequency
|
||||||
|
)
|
||||||
|
except PricingDataNotLoadedError:
|
||||||
|
log.info(
|
||||||
|
'pricing data for {symbol} not found on {dt}'
|
||||||
|
', updating the bundles.'.format(
|
||||||
|
symbol=[asset.symbol for asset in assets],
|
||||||
|
dt=dt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
bundle.ingest_assets(
|
||||||
|
assets=assets,
|
||||||
|
start_dt=self._first_trading_day,
|
||||||
|
end_dt=self._last_available_session,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
show_progress=True
|
||||||
|
)
|
||||||
|
return bundle.get_spot_values(
|
||||||
|
assets, field, dt, data_frequency, True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return bundle.get_spot_values(assets, field, dt, data_frequency)
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import sys, traceback
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
from catalyst.errors import ZiplineError
|
from catalyst.errors import ZiplineError
|
||||||
|
|
||||||
|
|
||||||
def silent_except_hook(exctype, excvalue, exctraceback):
|
def silent_except_hook(exctype, excvalue, exctraceback):
|
||||||
if exctype in [PricingDataBeforeTradingError, PricingDataNotLoadedError,
|
if exctype in [PricingDataBeforeTradingError, PricingDataNotLoadedError,
|
||||||
SymbolNotFoundOnExchange, NoDataAvailableOnExchange, ]:
|
SymbolNotFoundOnExchange, NoDataAvailableOnExchange,
|
||||||
|
ExchangeAuthEmpty]:
|
||||||
fn = traceback.extract_tb(exctraceback)[-1][0]
|
fn = traceback.extract_tb(exctraceback)[-1][0]
|
||||||
ln = traceback.extract_tb(exctraceback)[-1][1]
|
ln = traceback.extract_tb(exctraceback)[-1][1]
|
||||||
print "Error traceback: {1} (line {2})\n" \
|
print("Error traceback: {1} (line {2})\n"
|
||||||
"{0.__name__}: {3}".format(exctype, fn, ln, excvalue)
|
"{0.__name__}: {3}".format(exctype, fn, ln, excvalue))
|
||||||
else:
|
else:
|
||||||
sys.__excepthook__(exctype, excvalue, exctraceback)
|
sys.__excepthook__(exctype, excvalue, exctraceback)
|
||||||
|
|
||||||
@@ -63,6 +66,13 @@ class ExchangeAuthNotFound(ZiplineError):
|
|||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeAuthEmpty(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Please enter your API token key and secret for exchange {exchange} '
|
||||||
|
'in the following file: {filename}'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class ExchangeSymbolsNotFound(ZiplineError):
|
class ExchangeSymbolsNotFound(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Unable to download or find a local copy of symbols.json for exchange '
|
'Unable to download or find a local copy of symbols.json for exchange '
|
||||||
@@ -76,12 +86,33 @@ class AlgoPickleNotFound(ZiplineError):
|
|||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidHistoryFrequencyAlias(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Invalid frequency alias {freq}. Valid suffixes are M (minute) '
|
||||||
|
'and D (day). For example, these aliases would be valid '
|
||||||
|
'1M, 5M, 1D.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class InvalidHistoryFrequencyError(ZiplineError):
|
class InvalidHistoryFrequencyError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Frequency {frequency} not supported by the exchange.'
|
'Frequency {frequency} not supported by the exchange.'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedHistoryFrequencyError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'{exchange} does not support candle frequency {freq}, please choose '
|
||||||
|
'from: {freqs}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidHistoryTimeframeError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'CCXT timeframe {timeframe} not supported by the exchange.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class MismatchingFrequencyError(ZiplineError):
|
class MismatchingFrequencyError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Bar aggregate frequency {frequency} not compatible with '
|
'Bar aggregate frequency {frequency} not compatible with '
|
||||||
@@ -125,7 +156,8 @@ class OrphanOrderError(ZiplineError):
|
|||||||
|
|
||||||
class OrphanOrderReverseError(ZiplineError):
|
class OrphanOrderReverseError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Order {order_id} tracked by algorithm, but not found in exchange {exchange}.'
|
'Order {order_id} tracked by algorithm, but not found in exchange '
|
||||||
|
'{exchange}.'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -143,8 +175,8 @@ class SidHashError(ZiplineError):
|
|||||||
|
|
||||||
class BaseCurrencyNotFoundError(ZiplineError):
|
class BaseCurrencyNotFoundError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Algorithm base currency {base_currency} not found in exchange '
|
'Algorithm base currency {base_currency} not found in account '
|
||||||
'{exchange}.'
|
'balances on {exchange}: {balances}'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -188,23 +220,112 @@ class EmptyValuesInBundleError(ZiplineError):
|
|||||||
|
|
||||||
class PricingDataBeforeTradingError(ZiplineError):
|
class PricingDataBeforeTradingError(ZiplineError):
|
||||||
msg = ('Pricing data for trading pairs {symbols} on exchange {exchange} '
|
msg = ('Pricing data for trading pairs {symbols} on exchange {exchange} '
|
||||||
'starts on {first_trading_day}, but you are either trying to trade or '
|
'starts on {first_trading_day}, but you are either trying to trade '
|
||||||
'retrieve pricing data on {dt}. Adjust your dates accordingly.').strip()
|
'or retrieve pricing data on {dt}. Adjust your dates accordingly.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class PricingDataNotLoadedError(ZiplineError):
|
class PricingDataNotLoadedError(ZiplineError):
|
||||||
msg = ('Pricing data {field} for trading pairs {symbols} trading on '
|
msg = ('Missing data for {exchange} {symbols} in date range '
|
||||||
'exchange {exchange} since {first_trading_day} is unavailable. '
|
'[{start_dt} - {end_dt}]'
|
||||||
'The bundle data is either out-of-date or has not been loaded yet. '
|
'\nPlease run: `catalyst ingest-exchange -x {exchange} -f '
|
||||||
'Please ingest data using the command '
|
'{data_frequency} -i {symbol_list}`. See catalyst documentation '
|
||||||
'`catalyst ingest-exchange -x {exchange} -f {data_frequency} -i {symbol_list}`. '
|
'for details.').strip()
|
||||||
'See catalyst documentation for details.').strip()
|
|
||||||
|
|
||||||
|
class PricingDataValueError(ZiplineError):
|
||||||
|
msg = ('Unable to retrieve pricing data for {exchange} {symbol} '
|
||||||
|
'[{start_dt} - {end_dt}]: {error}').strip()
|
||||||
|
|
||||||
|
|
||||||
|
class DataCorruptionError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Unable to validate data for {exchange} {symbols} in date range '
|
||||||
|
'[{start_dt} - {end_dt}]. The data is either corrupted or '
|
||||||
|
'unavailable. Please try deleting this bundle:'
|
||||||
|
'\n`catalyst clean-exchange -x {exchange}\n'
|
||||||
|
'Then, ingest the data again. Please contact the Catalyst team if '
|
||||||
|
'the issue persists.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class ApiCandlesError(ZiplineError):
|
class ApiCandlesError(ZiplineError):
|
||||||
msg = ('Unable to fetch candles from the remote API: {error}.').strip()
|
msg = (
|
||||||
|
'Unable to fetch candles from the remote API: {error}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class NoDataAvailableOnExchange(ZiplineError):
|
class NoDataAvailableOnExchange(ZiplineError):
|
||||||
msg = ('Requested data for trading pair {symbol} is not available on exchange {exchange} '
|
msg = (
|
||||||
'in `{data_frequency}` frequency at this time. '
|
'Requested data for trading pair {symbol} is not available on '
|
||||||
'Check `http://enigma.co/catalyst/status` for market coverage.').strip()
|
'exchange {exchange} '
|
||||||
|
'in `{data_frequency}` frequency at this time. '
|
||||||
|
'Check `http://enigma.co/catalyst/status` for market coverage.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class NoValueForField(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Value not found for field: {field}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class OrderTypeNotSupported(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Order type `{order_type}` not currency supported by Catalyst. '
|
||||||
|
'Please use `limit` or `market` orders only.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class NotEnoughCapitalError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Not enough capital on exchange {exchange} for trading. Each '
|
||||||
|
'exchange should contain at least as much {base_currency} '
|
||||||
|
'as the specified `capital_base`. The current balance {balance} is '
|
||||||
|
'lower than the `capital_base`: {capital_base}'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class NotEnoughCashError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Total {currency} amount on {exchange} is lower than the cash '
|
||||||
|
'reserved for this algo: {free} < {cash}. While trades can be made on '
|
||||||
|
'the exchange accounts outside of the algo, exchange must have enough '
|
||||||
|
'free {currency} to cover the algo cash.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class LastCandleTooEarlyError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'The trade date of the last candle {last_traded} is before the '
|
||||||
|
'specified end date minus one candle {end_dt}. Please verify how '
|
||||||
|
'{exchange} calculates the start date of OHLCV candles.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class TickerNotFoundError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Unable to fetch ticker for {symbol} on {exchange}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class BalanceNotFoundError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'{currency} not found in account balance on {exchange}: {balances}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class BalanceTooLowError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Balance for {currency} on {exchange} too low: {free} < {amount}. '
|
||||||
|
'Positions have likely been sold outside of this algorithm. Please '
|
||||||
|
'add positions to hold a free amount greater than {amount}, or clean '
|
||||||
|
'the state of this algo and restart.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class NoCandlesReceivedFromExchange(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Although requesting {bar_count} candles until {end_dt} of asset {asset}, '
|
||||||
|
'an empty list of candles was received for {exchange}.'
|
||||||
|
).strip()
|
||||||
|
|||||||
@@ -4,9 +4,16 @@ from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder
|
|||||||
class ExchangeLimitOrder(LimitOrder):
|
class ExchangeLimitOrder(LimitOrder):
|
||||||
def get_limit_price(self, is_buy):
|
def get_limit_price(self, is_buy):
|
||||||
"""
|
"""
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
||||||
:param is_buy:
|
|
||||||
:return:
|
Parameters
|
||||||
|
----------
|
||||||
|
is_buy: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.limit_price
|
return self.limit_price
|
||||||
|
|
||||||
@@ -14,9 +21,16 @@ class ExchangeLimitOrder(LimitOrder):
|
|||||||
class ExchangeStopOrder(StopOrder):
|
class ExchangeStopOrder(StopOrder):
|
||||||
def get_stop_price(self, is_buy):
|
def get_stop_price(self, is_buy):
|
||||||
"""
|
"""
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
||||||
:param is_buy:
|
|
||||||
:return:
|
Parameters
|
||||||
|
----------
|
||||||
|
is_buy: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.stop_price
|
return self.stop_price
|
||||||
|
|
||||||
@@ -24,16 +38,30 @@ class ExchangeStopOrder(StopOrder):
|
|||||||
class ExchangeStopLimitOrder(StopLimitOrder):
|
class ExchangeStopLimitOrder(StopLimitOrder):
|
||||||
def get_limit_price(self, is_buy):
|
def get_limit_price(self, is_buy):
|
||||||
"""
|
"""
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
||||||
:param is_buy:
|
|
||||||
:return:
|
Parameters
|
||||||
|
----------
|
||||||
|
is_buy: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.limit_price
|
return self.limit_price
|
||||||
|
|
||||||
def get_stop_price(self, is_buy):
|
def get_stop_price(self, is_buy):
|
||||||
"""
|
"""
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
||||||
:param is_buy:
|
|
||||||
:return:
|
Parameters
|
||||||
|
----------
|
||||||
|
is_buy: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.stop_price
|
return self.stop_price
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
from catalyst.protocol import Portfolio, Positions, Position
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.protocol import Portfolio, Positions, Position
|
log = Logger('ExchangePortfolio', level=LOG_LEVEL)
|
||||||
|
|
||||||
log = Logger('ExchangePortfolio')
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangePortfolio(Portfolio):
|
class ExchangePortfolio(Portfolio):
|
||||||
"""
|
"""
|
||||||
Since the goal is to support multiple exchanges, it makes sense to
|
Since the goal is to support multiple exchanges, it makes sense to
|
||||||
include additional stats in the portfolio object.
|
include additional stats in the portfolio object. This fills the role
|
||||||
|
of Blotter and Portfolio in live mode.
|
||||||
|
|
||||||
Instead of relying on the performance tracker, each exchange portfolio
|
Instead of relying on the performance tracker, each exchange portfolio
|
||||||
tracks its own holding. This offers a separation between tracking an
|
tracks its own holding. This offers a separation between tracking an
|
||||||
@@ -28,12 +29,23 @@ class ExchangePortfolio(Portfolio):
|
|||||||
self.positions_value = 0.0
|
self.positions_value = 0.0
|
||||||
self.open_orders = dict()
|
self.open_orders = dict()
|
||||||
|
|
||||||
def calculate_pnl(self):
|
|
||||||
log.debug('calculating pnl')
|
|
||||||
|
|
||||||
def create_order(self, order):
|
def create_order(self, order):
|
||||||
|
"""
|
||||||
|
Create an open order and store in memory.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order: Order
|
||||||
|
|
||||||
|
"""
|
||||||
log.debug('creating order {}'.format(order.id))
|
log.debug('creating order {}'.format(order.id))
|
||||||
self.open_orders[order.id] = order
|
|
||||||
|
open_orders = self.open_orders[order.asset] \
|
||||||
|
if order.asset is self.open_orders else []
|
||||||
|
|
||||||
|
open_orders.append(order)
|
||||||
|
|
||||||
|
self.open_orders[order.asset] = open_orders
|
||||||
|
|
||||||
order_position = self.positions[order.asset] \
|
order_position = self.positions[order.asset] \
|
||||||
if order.asset in self.positions else None
|
if order.asset in self.positions else None
|
||||||
@@ -45,16 +57,40 @@ class ExchangePortfolio(Portfolio):
|
|||||||
order_position.amount += order.amount
|
order_position.amount += order.amount
|
||||||
log.debug('open order added to portfolio')
|
log.debug('open order added to portfolio')
|
||||||
|
|
||||||
|
def _remove_open_order(self, order):
|
||||||
|
try:
|
||||||
|
open_orders = self.open_orders[order.asset]
|
||||||
|
if order in open_orders:
|
||||||
|
open_orders.remove(order)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
raise ValueError(
|
||||||
|
'unable to clear order not found in open order list.'
|
||||||
|
)
|
||||||
|
|
||||||
def execute_order(self, order, transaction):
|
def execute_order(self, order, transaction):
|
||||||
|
"""
|
||||||
|
Update the open orders and positions to apply an executed order.
|
||||||
|
|
||||||
|
Unlike with backtesting, we do not need to add slippage and fees.
|
||||||
|
The executed price includes transaction fees.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order: Order
|
||||||
|
transaction: Transaction
|
||||||
|
|
||||||
|
"""
|
||||||
log.debug('executing order {}'.format(order.id))
|
log.debug('executing order {}'.format(order.id))
|
||||||
del self.open_orders[order.id]
|
self._remove_open_order(order)
|
||||||
|
|
||||||
order_position = self.positions[order.asset] \
|
order_position = self.positions[order.asset] \
|
||||||
if order.asset in self.positions else None
|
if order.asset in self.positions else None
|
||||||
|
|
||||||
if order_position is None:
|
if order_position is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Trying to execute order for a position not held: %s' % order.id
|
'Trying to execute order for a position not held:'
|
||||||
|
' {}'.format(order.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.capital_used += order.amount * transaction.price
|
self.capital_used += order.amount * transaction.price
|
||||||
@@ -70,33 +106,17 @@ class ExchangePortfolio(Portfolio):
|
|||||||
|
|
||||||
log.debug('updated portfolio with executed order')
|
log.debug('updated portfolio with executed order')
|
||||||
|
|
||||||
def execute_transaction(self, transaction):
|
|
||||||
log.debug('executing transaction {}'.format(transaction.order_id))
|
|
||||||
|
|
||||||
order_position = self.positions[transaction.asset] \
|
|
||||||
if transaction.asset in self.positions else None
|
|
||||||
|
|
||||||
if order_position is None:
|
|
||||||
raise ValueError(
|
|
||||||
'Trying to execute transaction for a position not held: %s' % transaction.order_id
|
|
||||||
)
|
|
||||||
|
|
||||||
self.capital_used += transaction.amount * transaction.price
|
|
||||||
|
|
||||||
if transaction.amount > 0:
|
|
||||||
if order_position.cost_basis > 0:
|
|
||||||
order_position.cost_basis = np.average(
|
|
||||||
[order_position.cost_basis, transaction.price],
|
|
||||||
weights=[order_position.amount, transaction.amount]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
order_position.cost_basis = transaction.price
|
|
||||||
|
|
||||||
log.debug('updated portfolio with executed order')
|
|
||||||
|
|
||||||
def remove_order(self, order):
|
def remove_order(self, order):
|
||||||
|
"""
|
||||||
|
Removing an open order.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order: Order
|
||||||
|
|
||||||
|
"""
|
||||||
log.info('removing cancelled order {}'.format(order.id))
|
log.info('removing cancelled order {}'.format(order.id))
|
||||||
del self.open_orders[order.id]
|
self._remove_open_order(order)
|
||||||
|
|
||||||
order_position = self.positions[order.asset] \
|
order_position = self.positions[order.asset] \
|
||||||
if order.asset in self.positions else None
|
if order.asset in self.positions else None
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# Copyright 2015 Quantopian, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
from catalyst.data.us_equity_pricing import BcolzDailyBarReader
|
||||||
|
from catalyst.errors import NoFurtherDataError
|
||||||
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
|
from catalyst.lib.adjusted_array import AdjustedArray
|
||||||
|
from catalyst.pipeline.data import DataSet, Column
|
||||||
|
from catalyst.pipeline.loaders.base import PipelineLoader
|
||||||
|
from catalyst.utils.calendars import get_calendar
|
||||||
|
from catalyst.utils.numpy_utils import float64_dtype
|
||||||
|
from logbook import Logger
|
||||||
|
from numpy import (
|
||||||
|
iinfo,
|
||||||
|
uint32,
|
||||||
|
)
|
||||||
|
|
||||||
|
UINT32_MAX = iinfo(uint32).max
|
||||||
|
|
||||||
|
log = Logger('ExchangePriceLoader', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
|
class TradingPairPricing(DataSet):
|
||||||
|
"""
|
||||||
|
Dataset representing daily trading prices and volumes.
|
||||||
|
"""
|
||||||
|
open = Column(float64_dtype)
|
||||||
|
high = Column(float64_dtype)
|
||||||
|
low = Column(float64_dtype)
|
||||||
|
close = Column(float64_dtype)
|
||||||
|
volume = Column(float64_dtype)
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangePricingLoader(PipelineLoader):
|
||||||
|
"""
|
||||||
|
PipelineLoader for Crypto Pricing data
|
||||||
|
|
||||||
|
Delegates loading of baselines and adjustments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data_frequency):
|
||||||
|
|
||||||
|
cal = get_calendar('OPEN')
|
||||||
|
|
||||||
|
if data_frequency == 'daily':
|
||||||
|
reader = None
|
||||||
|
all_sessions = cal.all_sessions
|
||||||
|
|
||||||
|
elif data_frequency == 'minute':
|
||||||
|
reader = None
|
||||||
|
all_sessions = cal.all_minutes
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Invalid data frequency: {}'.format(data_frequency)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.data_frequency = data_frequency
|
||||||
|
self.raw_price_loader = reader
|
||||||
|
self._columns = TradingPairPricing.columns
|
||||||
|
self._all_sessions = all_sessions
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_files(cls, pricing_path):
|
||||||
|
"""
|
||||||
|
Create a loader from a bcolz equity pricing dir and a SQLite
|
||||||
|
adjustments path.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
pricing_path : str
|
||||||
|
Path to a bcolz directory written by a BcolzDailyBarWriter.
|
||||||
|
"""
|
||||||
|
return cls(
|
||||||
|
BcolzDailyBarReader(pricing_path),
|
||||||
|
)
|
||||||
|
|
||||||
|
def load_adjusted_array(self, columns, dates, assets, mask):
|
||||||
|
# load_adjusted_array is called with dates on which the user's algo
|
||||||
|
# will be shown data, which means we need to return the data that would
|
||||||
|
# be known at the start of each date. We assume that the latest data
|
||||||
|
# known on day N is the data from day (N - 1), so we shift all query
|
||||||
|
# dates back by a day.
|
||||||
|
start_date, end_date = _shift_dates(
|
||||||
|
self._all_sessions, dates[0], dates[-1], shift=1,
|
||||||
|
)
|
||||||
|
colnames = [c.name for c in columns]
|
||||||
|
|
||||||
|
if len(assets) == 0:
|
||||||
|
raise ValueError(
|
||||||
|
'Pipeline cannot load data with eligible assets.'
|
||||||
|
)
|
||||||
|
|
||||||
|
exchange_names = []
|
||||||
|
for asset in assets:
|
||||||
|
if asset.exchange not in exchange_names:
|
||||||
|
exchange_names.append(asset.exchange)
|
||||||
|
|
||||||
|
exchange = get_exchange(exchange_names[0])
|
||||||
|
reader = exchange.bundle.get_reader(self.data_frequency)
|
||||||
|
|
||||||
|
raw_arrays = reader.load_raw_arrays(
|
||||||
|
colnames,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
assets,
|
||||||
|
)
|
||||||
|
|
||||||
|
out = {}
|
||||||
|
for c, c_raw in zip(columns, raw_arrays):
|
||||||
|
out[c] = AdjustedArray(
|
||||||
|
c_raw.astype(c.dtype),
|
||||||
|
mask,
|
||||||
|
{},
|
||||||
|
c.missing_value,
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
@property
|
||||||
|
def columns(self):
|
||||||
|
return self._columns
|
||||||
|
|
||||||
|
|
||||||
|
def _shift_dates(dates, start_date, end_date, shift):
|
||||||
|
try:
|
||||||
|
start = dates.get_loc(start_date)
|
||||||
|
except KeyError:
|
||||||
|
if start_date < dates[0]:
|
||||||
|
raise NoFurtherDataError(
|
||||||
|
msg=(
|
||||||
|
"Pipeline Query requested data starting on {query_start}, "
|
||||||
|
"but first known date is {calendar_start}"
|
||||||
|
).format(
|
||||||
|
query_start=str(start_date),
|
||||||
|
calendar_start=str(dates[0]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("Query start %s not in calendar" % start_date)
|
||||||
|
|
||||||
|
# Make sure that shifting doesn't push us out of the calendar.
|
||||||
|
if start < shift:
|
||||||
|
raise NoFurtherDataError(
|
||||||
|
msg=(
|
||||||
|
"Pipeline Query requested data from {shift}"
|
||||||
|
" days before {query_start}, but first known date is only "
|
||||||
|
"{start} days earlier."
|
||||||
|
).format(shift=shift, query_start=start_date, start=start),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
end = dates.get_loc(end_date)
|
||||||
|
except KeyError:
|
||||||
|
if end_date > dates[-1]:
|
||||||
|
raise NoFurtherDataError(
|
||||||
|
msg=(
|
||||||
|
"Pipeline Query requesting data up to {query_end}, "
|
||||||
|
"but last known date is {calendar_end}"
|
||||||
|
).format(
|
||||||
|
query_end=end_date,
|
||||||
|
calendar_end=dates[-1],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError("Query end %s not in calendar" % end_date)
|
||||||
|
return dates[start - shift], dates[end - shift]
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import pickle
|
|
||||||
import urllib
|
|
||||||
from datetime import date, datetime
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from catalyst.exchange.exchange_errors import ExchangeAuthNotFound, \
|
|
||||||
ExchangeSymbolsNotFound
|
|
||||||
from catalyst.utils.paths import data_root, ensure_directory, last_modified_time
|
|
||||||
|
|
||||||
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
|
||||||
'{exchange}/symbols.json'
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_folder(exchange_name, environ=None):
|
|
||||||
if not environ:
|
|
||||||
environ = os.environ
|
|
||||||
|
|
||||||
root = data_root(environ)
|
|
||||||
exchange_folder = os.path.join(root, 'exchanges', exchange_name)
|
|
||||||
ensure_directory(exchange_folder)
|
|
||||||
|
|
||||||
return exchange_folder
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_symbols_filename(exchange_name, environ=None):
|
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
|
||||||
return os.path.join(exchange_folder, 'symbols.json')
|
|
||||||
|
|
||||||
|
|
||||||
def download_exchange_symbols(exchange_name, environ=None):
|
|
||||||
filename = get_exchange_symbols_filename(exchange_name)
|
|
||||||
url = SYMBOLS_URL.format(exchange=exchange_name)
|
|
||||||
response = urllib.urlretrieve(url=url, filename=filename)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_symbols(exchange_name, environ=None):
|
|
||||||
filename = get_exchange_symbols_filename(exchange_name)
|
|
||||||
|
|
||||||
if not os.path.isfile(filename) or \
|
|
||||||
pd.Timedelta(pd.Timestamp('now', tz='UTC') - last_modified_time(filename)).days > 1:
|
|
||||||
download_exchange_symbols(exchange_name, environ)
|
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
|
||||||
with open(filename) as data_file:
|
|
||||||
data = json.load(data_file)
|
|
||||||
return data
|
|
||||||
else:
|
|
||||||
raise ExchangeSymbolsNotFound(
|
|
||||||
exchange=exchange_name,
|
|
||||||
filename=filename
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_auth(exchange_name, environ=None):
|
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
|
||||||
filename = os.path.join(exchange_folder, 'auth.json')
|
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
|
||||||
with open(filename) as data_file:
|
|
||||||
data = json.load(data_file)
|
|
||||||
return data
|
|
||||||
else:
|
|
||||||
raise ExchangeAuthNotFound(
|
|
||||||
exchange=exchange_name,
|
|
||||||
filename=filename
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_algo_folder(algo_name, environ=None):
|
|
||||||
if not environ:
|
|
||||||
environ = os.environ
|
|
||||||
|
|
||||||
root = data_root(environ)
|
|
||||||
algo_folder = os.path.join(root, 'live_algos', algo_name)
|
|
||||||
ensure_directory(algo_folder)
|
|
||||||
|
|
||||||
return algo_folder
|
|
||||||
|
|
||||||
|
|
||||||
def get_algo_object(algo_name, key, environ=None, rel_path=None):
|
|
||||||
if algo_name is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
folder = get_algo_folder(algo_name, environ)
|
|
||||||
|
|
||||||
if rel_path is not None:
|
|
||||||
folder = os.path.join(folder, rel_path)
|
|
||||||
|
|
||||||
filename = os.path.join(folder, key + '.p')
|
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
|
||||||
try:
|
|
||||||
with open(filename, 'rb') as handle:
|
|
||||||
return pickle.load(handle)
|
|
||||||
except Exception as e:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def save_algo_object(algo_name, key, obj, environ=None, rel_path=None):
|
|
||||||
folder = get_algo_folder(algo_name, environ)
|
|
||||||
|
|
||||||
if rel_path is not None:
|
|
||||||
folder = os.path.join(folder, rel_path)
|
|
||||||
ensure_directory(folder)
|
|
||||||
|
|
||||||
filename = os.path.join(folder, key + '.p')
|
|
||||||
|
|
||||||
with open(filename, 'wb') as handle:
|
|
||||||
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
|
||||||
|
|
||||||
|
|
||||||
def append_algo_object(algo_name, key, obj, environ=None):
|
|
||||||
algo_folder = get_algo_folder(algo_name, environ)
|
|
||||||
filename = os.path.join(algo_folder, key + '.p')
|
|
||||||
|
|
||||||
mode = 'a+b' if os.path.isfile(filename) else 'wb'
|
|
||||||
with open(filename, mode) as handle:
|
|
||||||
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
|
||||||
|
|
||||||
|
|
||||||
def get_algo_df(algo_name, key, environ=None, rel_path=None):
|
|
||||||
folder = get_algo_folder(algo_name, environ)
|
|
||||||
|
|
||||||
if rel_path is not None:
|
|
||||||
folder = os.path.join(folder, rel_path)
|
|
||||||
|
|
||||||
filename = os.path.join(folder, key + '.csv')
|
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
|
||||||
try:
|
|
||||||
with open(filename, 'rb') as handle:
|
|
||||||
return pd.read_csv(handle, index_col=0, parse_dates=True)
|
|
||||||
except IOError:
|
|
||||||
return pd.DataFrame()
|
|
||||||
else:
|
|
||||||
return pd.DataFrame()
|
|
||||||
|
|
||||||
|
|
||||||
def save_algo_df(algo_name, key, df, environ=None, rel_path=None):
|
|
||||||
folder = get_algo_folder(algo_name, environ)
|
|
||||||
|
|
||||||
if rel_path is not None:
|
|
||||||
folder = os.path.join(folder, rel_path)
|
|
||||||
ensure_directory(folder)
|
|
||||||
|
|
||||||
filename = os.path.join(folder, key + '.csv')
|
|
||||||
|
|
||||||
with open(filename, 'wb') as handle:
|
|
||||||
df.to_csv(handle)
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_minute_writer_root(exchange_name, environ=None):
|
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
|
||||||
|
|
||||||
minute_data_folder = os.path.join(exchange_folder, 'minute_data')
|
|
||||||
ensure_directory(minute_data_folder)
|
|
||||||
|
|
||||||
return minute_data_folder
|
|
||||||
|
|
||||||
def get_exchange_bundles_folder(exchange_name, environ=None):
|
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
|
||||||
|
|
||||||
temp_bundles = os.path.join(exchange_folder, 'temp_bundles')
|
|
||||||
ensure_directory(temp_bundles)
|
|
||||||
|
|
||||||
return temp_bundles
|
|
||||||
|
|
||||||
|
|
||||||
def perf_serial(obj):
|
|
||||||
"""JSON serializer for objects not serializable by default json code"""
|
|
||||||
|
|
||||||
if isinstance(obj, (datetime, date)):
|
|
||||||
return obj.isoformat()
|
|
||||||
raise TypeError("Type %s not serializable" % type(obj))
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
|
|
||||||
from catalyst.exchange.bittrex.bittrex import Bittrex
|
|
||||||
from catalyst.exchange.exchange_errors import ExchangeNotFoundError
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_auth
|
|
||||||
from catalyst.exchange.poloniex.poloniex import Poloniex
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange(exchange_name):
|
|
||||||
exchange_auth = get_exchange_auth(exchange_name)
|
|
||||||
if exchange_name == 'bitfinex':
|
|
||||||
return Bitfinex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=None, # TODO: make optional at the exchange
|
|
||||||
portfolio=None
|
|
||||||
)
|
|
||||||
elif exchange_name == 'bittrex':
|
|
||||||
return Bittrex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=None,
|
|
||||||
portfolio=None
|
|
||||||
)
|
|
||||||
elif exchange_name == 'poloniex':
|
|
||||||
return Poloniex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=None,
|
|
||||||
portfolio=None
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
|
||||||
@@ -1,29 +1,13 @@
|
|||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
from catalyst.exchange.utils.stats_utils import prepare_stats
|
||||||
from catalyst.gens.sim_engine import (
|
from catalyst.gens.sim_engine import (
|
||||||
BAR,
|
BAR,
|
||||||
SESSION_START
|
SESSION_START
|
||||||
)
|
)
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.exchange.exchange_errors import \
|
log = Logger('LiveGraphClock', level=LOG_LEVEL)
|
||||||
MismatchingBaseCurrenciesExchanges
|
|
||||||
|
|
||||||
|
|
||||||
log = Logger('LiveGraphClock')
|
|
||||||
|
|
||||||
|
|
||||||
class LiveGraphClock(object):
|
class LiveGraphClock(object):
|
||||||
@@ -34,8 +18,8 @@ class LiveGraphClock(object):
|
|||||||
|
|
||||||
This mixes the clock with a live graph.
|
This mixes the clock with a live graph.
|
||||||
|
|
||||||
Note
|
Notes
|
||||||
----
|
-----
|
||||||
This seemingly awkward approach allows us to run the program using a single
|
This seemingly awkward approach allows us to run the program using a single
|
||||||
thread. This is important because Matplotlib does not play nice with
|
thread. This is important because Matplotlib does not play nice with
|
||||||
multi-threaded environments. Zipline probably does not either.
|
multi-threaded environments. Zipline probably does not either.
|
||||||
@@ -52,156 +36,23 @@ class LiveGraphClock(object):
|
|||||||
the exchange and the live trading machine's clock. It's not used currently.
|
the exchange and the live trading machine's clock. It's not used currently.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, sessions, context, time_skew=pd.Timedelta('0s')):
|
def __init__(self, sessions, context, callback=None,
|
||||||
|
time_skew=pd.Timedelta('0s')):
|
||||||
global mdates, plt #TODO: Could be cleaner
|
|
||||||
import matplotlib.dates as mdates
|
|
||||||
from matplotlib import pyplot as plt
|
|
||||||
from matplotlib import style
|
|
||||||
|
|
||||||
self.sessions = sessions
|
self.sessions = sessions
|
||||||
self.time_skew = time_skew
|
self.time_skew = time_skew
|
||||||
self._last_emit = None
|
self._last_emit = None
|
||||||
self._before_trading_start_bar_yielded = True
|
self._before_trading_start_bar_yielded = True
|
||||||
self.context = context
|
self.context = context
|
||||||
self.fmt = mdates.DateFormatter('%Y-%m-%d %H:%M')
|
self.callback = callback
|
||||||
|
|
||||||
style.use('dark_background')
|
|
||||||
|
|
||||||
fig = plt.figure()
|
|
||||||
fig.canvas.set_window_title('Enigma Catalyst: {}'.format(
|
|
||||||
self.context.algo_namespace))
|
|
||||||
|
|
||||||
self.ax_pnl = fig.add_subplot(311)
|
|
||||||
|
|
||||||
self.ax_custom_signals = fig.add_subplot(312, sharex=self.ax_pnl)
|
|
||||||
|
|
||||||
self.ax_exposure = fig.add_subplot(313, sharex=self.ax_pnl)
|
|
||||||
|
|
||||||
if len(context.minute_stats) > 0:
|
|
||||||
self.draw_pnl()
|
|
||||||
self.draw_custom_signals()
|
|
||||||
self.draw_exposure()
|
|
||||||
|
|
||||||
# rotates and right aligns the x labels, and moves the bottom of the
|
|
||||||
# axes up to make room for them
|
|
||||||
fig.autofmt_xdate()
|
|
||||||
fig.subplots_adjust(hspace=0.5)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.ion()
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
def format_ax(self, ax):
|
|
||||||
"""
|
|
||||||
Trying to assign reasonable parameters to the time axis.
|
|
||||||
|
|
||||||
TODO: room for improvement
|
|
||||||
|
|
||||||
:param ax:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
|
|
||||||
ax.xaxis.set_major_formatter(self.fmt)
|
|
||||||
|
|
||||||
locator = mdates.HourLocator(interval=4)
|
|
||||||
locator.MAXTICKS = 5000
|
|
||||||
ax.xaxis.set_minor_locator(locator)
|
|
||||||
|
|
||||||
datemin = pd.Timestamp.utcnow()
|
|
||||||
ax.set_xlim(datemin)
|
|
||||||
|
|
||||||
ax.grid(True)
|
|
||||||
|
|
||||||
def set_legend(self, ax):
|
|
||||||
ax.legend(loc='upper left', ncol=1, fontsize=10, numpoints=1)
|
|
||||||
|
|
||||||
def draw_pnl(self):
|
|
||||||
ax = self.ax_pnl
|
|
||||||
df = self.context.pnl_stats
|
|
||||||
|
|
||||||
ax.clear()
|
|
||||||
ax.set_title('Performance')
|
|
||||||
ax.plot(df.index, df['performance'], '-',
|
|
||||||
color='green',
|
|
||||||
linewidth=1.0,
|
|
||||||
label='Performance'
|
|
||||||
)
|
|
||||||
|
|
||||||
def perc(val):
|
|
||||||
return '{:2f}'.format(val)
|
|
||||||
|
|
||||||
ax.format_ydata = perc
|
|
||||||
|
|
||||||
self.set_legend(ax)
|
|
||||||
self.format_ax(ax)
|
|
||||||
|
|
||||||
def draw_custom_signals(self):
|
|
||||||
ax = self.ax_custom_signals
|
|
||||||
df = self.context.custom_signals_stats
|
|
||||||
|
|
||||||
colors = ['blue', 'green', 'red', 'black', 'orange', 'yellow', 'pink']
|
|
||||||
|
|
||||||
ax.clear()
|
|
||||||
ax.set_title('Custom Signals')
|
|
||||||
for index, column in enumerate(df.columns.values.tolist()):
|
|
||||||
ax.plot(df.index, df[column], '-',
|
|
||||||
color=colors[index],
|
|
||||||
linewidth=1.0,
|
|
||||||
label=column
|
|
||||||
)
|
|
||||||
|
|
||||||
self.set_legend(ax)
|
|
||||||
self.format_ax(ax)
|
|
||||||
|
|
||||||
def draw_exposure(self):
|
|
||||||
ax = self.ax_exposure
|
|
||||||
context = self.context
|
|
||||||
df = context.exposure_stats
|
|
||||||
|
|
||||||
# TODO: list exchanges in graph
|
|
||||||
base_currency = None
|
|
||||||
positions = []
|
|
||||||
for exchange_name in context.exchanges:
|
|
||||||
exchange = context.exchanges[exchange_name]
|
|
||||||
|
|
||||||
if not base_currency:
|
|
||||||
base_currency = exchange.base_currency
|
|
||||||
elif base_currency != exchange.base_currency:
|
|
||||||
raise MismatchingBaseCurrenciesExchanges(
|
|
||||||
base_currency=base_currency,
|
|
||||||
exchange_name=exchange.name,
|
|
||||||
exchange_currency=exchange.base_currency
|
|
||||||
)
|
|
||||||
|
|
||||||
positions += exchange.portfolio.positions
|
|
||||||
|
|
||||||
ax.clear()
|
|
||||||
ax.set_title('Exposure')
|
|
||||||
ax.plot(df.index, df['base_currency'], '-',
|
|
||||||
color='green',
|
|
||||||
linewidth=1.0,
|
|
||||||
label='Base Currency: {}'.format(base_currency.upper())
|
|
||||||
)
|
|
||||||
|
|
||||||
symbols = []
|
|
||||||
for position in positions:
|
|
||||||
symbols.append(position.symbol)
|
|
||||||
|
|
||||||
ax.plot(df.index, df['long_exposure'], '-',
|
|
||||||
color='blue',
|
|
||||||
linewidth=1.0,
|
|
||||||
label='Long Exposure: {}'.format(', '.join(symbols).upper()))
|
|
||||||
|
|
||||||
self.set_legend(ax)
|
|
||||||
self.format_ax(ax)
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
yield pd.Timestamp.utcnow(), SESSION_START
|
yield pd.Timestamp.utcnow(), SESSION_START
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
current_time = pd.Timestamp.utcnow()
|
current_time = pd.Timestamp.utcnow()
|
||||||
current_minute = current_time.floor('1 min')
|
current_minute = current_time.floor('1T')
|
||||||
|
|
||||||
if self._last_emit is None or current_minute > self._last_emit:
|
if self._last_emit is None or current_minute > self._last_emit:
|
||||||
log.debug('emitting minutely bar: {}'.format(current_minute))
|
log.debug('emitting minutely bar: {}'.format(current_minute))
|
||||||
@@ -209,14 +60,11 @@ class LiveGraphClock(object):
|
|||||||
self._last_emit = current_minute
|
self._last_emit = current_minute
|
||||||
yield current_minute, BAR
|
yield current_minute, BAR
|
||||||
|
|
||||||
try:
|
recorded_cols = list(self.context.recorded_vars.keys())
|
||||||
self.draw_pnl()
|
df, _ = prepare_stats(
|
||||||
self.draw_custom_signals()
|
self.context.frame_stats, recorded_cols=recorded_cols
|
||||||
self.draw_exposure()
|
)
|
||||||
|
self.callback(self.context, df)
|
||||||
plt.draw()
|
|
||||||
except Exception as e:
|
|
||||||
log.warn('Unable to update the graph: {}'.format(e))
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# I can't use the "animate" reactive approach here because
|
# I can't use the "animate" reactive approach here because
|
||||||
|
|||||||
@@ -1,638 +0,0 @@
|
|||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import pytz
|
|
||||||
import requests
|
|
||||||
# import six
|
|
||||||
from six import iteritems
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
|
||||||
from catalyst.exchange.poloniex.poloniex_api import Poloniex_api
|
|
||||||
|
|
||||||
# from websocket import create_connection
|
|
||||||
from catalyst.exchange.exchange import Exchange
|
|
||||||
from catalyst.exchange.exchange_errors import (
|
|
||||||
ExchangeRequestError,
|
|
||||||
InvalidHistoryFrequencyError,
|
|
||||||
InvalidOrderStyle, OrderCancelError,
|
|
||||||
OrphanOrderReverseError)
|
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
|
|
||||||
ExchangeStopLimitOrder, ExchangeStopOrder
|
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
|
||||||
from catalyst.protocol import Account
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
|
|
||||||
download_exchange_symbols
|
|
||||||
from catalyst.finance.transaction import Transaction
|
|
||||||
|
|
||||||
log = Logger('Poloniex')
|
|
||||||
|
|
||||||
|
|
||||||
class Poloniex(Exchange):
|
|
||||||
def __init__(self, key, secret, base_currency, portfolio=None):
|
|
||||||
self.api = Poloniex_api(key=key, secret=secret.encode('UTF-8'))
|
|
||||||
self.name = 'poloniex'
|
|
||||||
self.assets = {}
|
|
||||||
self.load_assets()
|
|
||||||
self.base_currency = base_currency
|
|
||||||
self._portfolio = portfolio
|
|
||||||
self.minute_writer = None
|
|
||||||
self.minute_reader = None
|
|
||||||
self.transactions = defaultdict(list)
|
|
||||||
|
|
||||||
self.num_candles_limit = 2000
|
|
||||||
self.max_requests_per_minute = 60
|
|
||||||
self.request_cpt = dict()
|
|
||||||
|
|
||||||
self.bundle = ExchangeBundle(self)
|
|
||||||
|
|
||||||
def sanitize_curency_symbol(self, exchange_symbol):
|
|
||||||
"""
|
|
||||||
Helper method used to build the universal pair.
|
|
||||||
Include any symbol mapping here if appropriate.
|
|
||||||
|
|
||||||
:param exchange_symbol:
|
|
||||||
:return universal_symbol:
|
|
||||||
"""
|
|
||||||
return exchange_symbol.lower()
|
|
||||||
|
|
||||||
def _create_order(self, order_status):
|
|
||||||
"""
|
|
||||||
Create a Catalyst order object from the Exchange order dictionary
|
|
||||||
:param order_status:
|
|
||||||
:return: Order
|
|
||||||
"""
|
|
||||||
# if order_status['is_cancelled']:
|
|
||||||
# status = ORDER_STATUS.CANCELLED
|
|
||||||
# elif not order_status['is_live']:
|
|
||||||
# log.info('found executed order {}'.format(order_status))
|
|
||||||
# status = ORDER_STATUS.FILLED
|
|
||||||
# else:
|
|
||||||
status = ORDER_STATUS.OPEN
|
|
||||||
|
|
||||||
amount = float(order_status['amount'])
|
|
||||||
# filled = float(order_status['executed_amount'])
|
|
||||||
filled = None
|
|
||||||
|
|
||||||
if order_status['type'] == 'sell':
|
|
||||||
amount = -amount
|
|
||||||
# filled = -filled
|
|
||||||
|
|
||||||
price = float(order_status['rate'])
|
|
||||||
order_type = order_status['type']
|
|
||||||
|
|
||||||
stop_price = None
|
|
||||||
limit_price = None
|
|
||||||
|
|
||||||
# TODO: is this comprehensive enough?
|
|
||||||
# if order_type.endswith('limit'):
|
|
||||||
# limit_price = price
|
|
||||||
# elif order_type.endswith('stop'):
|
|
||||||
# stop_price = price
|
|
||||||
|
|
||||||
# executed_price = float(order_status['avg_execution_price'])
|
|
||||||
executed_price = price
|
|
||||||
|
|
||||||
# TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it.
|
|
||||||
commission = None
|
|
||||||
|
|
||||||
# date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
|
|
||||||
# date = pytz.utc.localize(date)
|
|
||||||
date = None
|
|
||||||
|
|
||||||
order = Order(
|
|
||||||
dt=date,
|
|
||||||
asset=self.assets[order_status['symbol']],
|
|
||||||
# No such field in Poloniex
|
|
||||||
amount=amount,
|
|
||||||
stop=stop_price,
|
|
||||||
limit=limit_price,
|
|
||||||
filled=filled,
|
|
||||||
id=str(order_status['orderNumber']),
|
|
||||||
commission=commission
|
|
||||||
)
|
|
||||||
order.status = status
|
|
||||||
|
|
||||||
return order, executed_price
|
|
||||||
|
|
||||||
def get_balances(self):
|
|
||||||
log.debug('retrieving wallets balances')
|
|
||||||
try:
|
|
||||||
balances = self.api.returnbalances()
|
|
||||||
except Exception as e:
|
|
||||||
log.debug(e)
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'error' in balances:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='unable to fetch balance {}'.format(balances['error'])
|
|
||||||
)
|
|
||||||
|
|
||||||
std_balances = dict()
|
|
||||||
for (key, value) in iteritems(balances):
|
|
||||||
currency = key.lower()
|
|
||||||
std_balances[currency] = float(value)
|
|
||||||
|
|
||||||
return std_balances
|
|
||||||
|
|
||||||
@property
|
|
||||||
def account(self):
|
|
||||||
account = Account()
|
|
||||||
|
|
||||||
account.settled_cash = None
|
|
||||||
account.accrued_interest = None
|
|
||||||
account.buying_power = None
|
|
||||||
account.equity_with_loan = None
|
|
||||||
account.total_positions_value = None
|
|
||||||
account.total_positions_exposure = None
|
|
||||||
account.regt_equity = None
|
|
||||||
account.regt_margin = None
|
|
||||||
account.initial_margin_requirement = None
|
|
||||||
account.maintenance_margin_requirement = None
|
|
||||||
account.available_funds = None
|
|
||||||
account.excess_liquidity = None
|
|
||||||
account.cushion = None
|
|
||||||
account.day_trades_remaining = None
|
|
||||||
account.leverage = None
|
|
||||||
account.net_leverage = None
|
|
||||||
account.net_liquidation = None
|
|
||||||
|
|
||||||
return account
|
|
||||||
|
|
||||||
@property
|
|
||||||
def time_skew(self):
|
|
||||||
# TODO: research the time skew conditions
|
|
||||||
return pd.Timedelta('0s')
|
|
||||||
|
|
||||||
def get_account(self):
|
|
||||||
# TODO: fetch account data and keep in cache
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_candles(self, data_frequency, assets, bar_count=None,
|
|
||||||
start_dt=None, end_dt=None):
|
|
||||||
"""
|
|
||||||
Retrieve OHLVC candles from Poloniex
|
|
||||||
|
|
||||||
:param data_frequency:
|
|
||||||
:param assets:
|
|
||||||
:param bar_count:
|
|
||||||
:return:
|
|
||||||
|
|
||||||
Available Frequencies
|
|
||||||
---------------------
|
|
||||||
'5m', '15m', '30m', '2h', '4h', '1D'
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO: implement end_dt and start_dt filters
|
|
||||||
|
|
||||||
if (
|
|
||||||
data_frequency == '5m' or data_frequency == 'minute'): # TODO: Polo does not have '1m'
|
|
||||||
frequency = 300
|
|
||||||
elif (data_frequency == '15m'):
|
|
||||||
frequency = 900
|
|
||||||
elif (data_frequency == '30m'):
|
|
||||||
frequency = 1800
|
|
||||||
elif (data_frequency == '2h'):
|
|
||||||
frequency = 7200
|
|
||||||
elif (data_frequency == '4h'):
|
|
||||||
frequency = 14400
|
|
||||||
elif (data_frequency == '1D' or data_frequency == 'daily'):
|
|
||||||
frequency = 86400
|
|
||||||
else:
|
|
||||||
raise InvalidHistoryFrequencyError(
|
|
||||||
frequency=data_frequency
|
|
||||||
)
|
|
||||||
|
|
||||||
# Making sure that assets are iterable
|
|
||||||
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
|
||||||
ohlc_map = dict()
|
|
||||||
|
|
||||||
for asset in asset_list:
|
|
||||||
|
|
||||||
end = int(time.time())
|
|
||||||
if (bar_count is None):
|
|
||||||
start = end - 2 * frequency
|
|
||||||
else:
|
|
||||||
start = end - bar_count * frequency
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self.api.returnchartdata(self.get_symbol(asset),
|
|
||||||
frequency, start, end)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'error' in response:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='Unable to retrieve candles: {}'.format(
|
|
||||||
response.content)
|
|
||||||
)
|
|
||||||
|
|
||||||
def ohlc_from_candle(candle):
|
|
||||||
last_traded = pd.Timestamp.utcfromtimestamp(candle['date'])
|
|
||||||
last_traded = last_traded.replace(tzinfo=pytz.UTC)
|
|
||||||
|
|
||||||
ohlc = dict(
|
|
||||||
open=np.float64(candle['open']),
|
|
||||||
high=np.float64(candle['high']),
|
|
||||||
low=np.float64(candle['low']),
|
|
||||||
close=np.float64(candle['close']),
|
|
||||||
volume=np.float64(candle['volume']),
|
|
||||||
price=np.float64(candle['close']),
|
|
||||||
last_traded=last_traded
|
|
||||||
)
|
|
||||||
|
|
||||||
return ohlc
|
|
||||||
|
|
||||||
if bar_count is None:
|
|
||||||
ohlc_map[asset] = ohlc_from_candle(response[0])
|
|
||||||
else:
|
|
||||||
ohlc_bars = []
|
|
||||||
for candle in response:
|
|
||||||
ohlc = ohlc_from_candle(candle)
|
|
||||||
ohlc_bars.append(ohlc)
|
|
||||||
ohlc_map[asset] = ohlc_bars
|
|
||||||
|
|
||||||
return ohlc_map[assets] \
|
|
||||||
if isinstance(assets, TradingPair) else ohlc_map
|
|
||||||
|
|
||||||
def create_order(self, asset, amount, is_buy, style):
|
|
||||||
"""
|
|
||||||
Creating order on the exchange.
|
|
||||||
|
|
||||||
:param asset:
|
|
||||||
:param amount:
|
|
||||||
:param is_buy:
|
|
||||||
:param style:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
exchange_symbol = self.get_symbol(asset)
|
|
||||||
|
|
||||||
if isinstance(style, ExchangeLimitOrder) or isinstance(style,
|
|
||||||
ExchangeStopLimitOrder):
|
|
||||||
if isinstance(style, ExchangeStopLimitOrder):
|
|
||||||
log.warn('{} will ignore the stop price'.format(self.name))
|
|
||||||
|
|
||||||
price = style.get_limit_price(is_buy)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if (is_buy):
|
|
||||||
response = self.api.buy(exchange_symbol, amount, price)
|
|
||||||
else:
|
|
||||||
response = self.api.sell(exchange_symbol, -amount, price)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
date = pd.Timestamp.utcnow()
|
|
||||||
|
|
||||||
if ('orderNumber' in response):
|
|
||||||
order_id = str(response['orderNumber'])
|
|
||||||
order = Order(
|
|
||||||
dt=date,
|
|
||||||
asset=asset,
|
|
||||||
amount=amount,
|
|
||||||
stop=style.get_stop_price(is_buy),
|
|
||||||
limit=style.get_limit_price(is_buy),
|
|
||||||
id=order_id
|
|
||||||
)
|
|
||||||
return order
|
|
||||||
else:
|
|
||||||
log.warn(
|
|
||||||
'{} order failed: {}'.format('buy' if is_buy else 'sell',
|
|
||||||
response['error']))
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
raise InvalidOrderStyle(exchange=self.name,
|
|
||||||
style=style.__class__.__name__)
|
|
||||||
|
|
||||||
def get_open_orders(self, asset='all'):
|
|
||||||
"""Retrieve all of the current open orders.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
asset : Asset
|
|
||||||
If passed and not 'all', return only the open orders for the given
|
|
||||||
asset instead of all open orders.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
open_orders : dict[list[Order]] or list[Order]
|
|
||||||
If 'all' is passed this will return a dict mapping Assets
|
|
||||||
to a list containing all the open orders for the asset.
|
|
||||||
If an asset is passed then this will return a list of the open
|
|
||||||
orders for this asset.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.portfolio.open_orders
|
|
||||||
|
|
||||||
"""
|
|
||||||
TODO: Why going to the exchange if we already have this info locally?
|
|
||||||
And why creating all these Orders if we later discard them?
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
if (asset == 'all'):
|
|
||||||
response = self.api.returnopenorders('all')
|
|
||||||
else:
|
|
||||||
response = self.api.returnopenorders(self.get_symbol(asset))
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'error' in response:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='Unable to retrieve open orders: {}'.format(
|
|
||||||
order_statuses['message'])
|
|
||||||
)
|
|
||||||
|
|
||||||
print(self.portfolio.open_orders)
|
|
||||||
|
|
||||||
# TODO: Need to handle openOrders for 'all'
|
|
||||||
orders = list()
|
|
||||||
for order_status in response:
|
|
||||||
order, executed_price = self._create_order(
|
|
||||||
order_status) # will Throw error b/c Polo doesn't track order['symbol']
|
|
||||||
if asset is None or asset == order.sid:
|
|
||||||
orders.append(order)
|
|
||||||
|
|
||||||
return orders
|
|
||||||
|
|
||||||
def get_order(self, order_id):
|
|
||||||
"""Lookup an order based on the order id returned from one of the
|
|
||||||
order functions.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
order_id : str
|
|
||||||
The unique identifier for the order.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
order : Order
|
|
||||||
The order object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
order = self._portfolio.open_orders[order_id]
|
|
||||||
except Exception as e:
|
|
||||||
raise OrphanOrderError(order_id=order_id, exchange=self.name)
|
|
||||||
|
|
||||||
return order
|
|
||||||
|
|
||||||
# TODO: Need to decide whether we fetch orders locally or from exchnage
|
|
||||||
# The code below is ignored
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self.api.returnopenorders(self.get_symbol(order.sid))
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
for o in response:
|
|
||||||
if (int(o['orderNumber']) == int(order_id)):
|
|
||||||
return order
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def cancel_order(self, order_param):
|
|
||||||
"""Cancel an open order.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
order_param : str or Order
|
|
||||||
The order_id or order object to cancel.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if (isinstance(order_param, Order)):
|
|
||||||
order = order_param
|
|
||||||
else:
|
|
||||||
order = self._portfolio.open_orders[order_param]
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self.api.cancelorder(order.id)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'error' in response:
|
|
||||||
log.info(
|
|
||||||
'Unable to cancel order {order_id} on exchange {exchange} {error}.'.format(
|
|
||||||
order_id=order.id,
|
|
||||||
exchange=self.name,
|
|
||||||
error=response['error']
|
|
||||||
))
|
|
||||||
|
|
||||||
# raise OrderCancelError(
|
|
||||||
# order_id=order.id,
|
|
||||||
# exchange=self.name,
|
|
||||||
# error=response['error']
|
|
||||||
# )
|
|
||||||
|
|
||||||
self.portfolio.remove_order(order)
|
|
||||||
|
|
||||||
def tickers(self, assets):
|
|
||||||
"""
|
|
||||||
Fetch ticket data for assets
|
|
||||||
https://docs.bitfinex.com/v2/reference#rest-public-tickers
|
|
||||||
|
|
||||||
:param assets:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
symbols = self.get_symbols(assets)
|
|
||||||
|
|
||||||
log.debug('fetching tickers {}'.format(symbols))
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self.api.returnticker()
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'error' in response:
|
|
||||||
raise ExchangeRequestError(
|
|
||||||
error='Unable to retrieve tickers: {}'.format(
|
|
||||||
response['error'])
|
|
||||||
)
|
|
||||||
|
|
||||||
ticks = dict()
|
|
||||||
|
|
||||||
for index, symbol in enumerate(symbols):
|
|
||||||
ticks[assets[index]] = dict(
|
|
||||||
timestamp=pd.Timestamp.utcnow(),
|
|
||||||
bid=float(response[symbol]['highestBid']),
|
|
||||||
ask=float(response[symbol]['lowestAsk']),
|
|
||||||
last_price=float(response[symbol]['last']),
|
|
||||||
low=float(response[symbol]['lowestAsk']),
|
|
||||||
# TODO: Polo does not provide low
|
|
||||||
high=float(response[symbol]['highestBid']),
|
|
||||||
# TODO: Polo does not provide high
|
|
||||||
volume=float(response[symbol]['baseVolume']),
|
|
||||||
)
|
|
||||||
|
|
||||||
log.debug('got tickers {}'.format(ticks))
|
|
||||||
return ticks
|
|
||||||
|
|
||||||
def generate_symbols_json(self, filename=None, source_dates=False):
|
|
||||||
symbol_map = {}
|
|
||||||
|
|
||||||
if not source_dates:
|
|
||||||
fn, r = download_exchange_symbols(self.name)
|
|
||||||
with open(fn) as data_file:
|
|
||||||
cached_symbols = json.load(data_file)
|
|
||||||
|
|
||||||
response = self.api.returnticker()
|
|
||||||
|
|
||||||
for exchange_symbol in response:
|
|
||||||
base, market = self.sanitize_curency_symbol(exchange_symbol).split(
|
|
||||||
'_')
|
|
||||||
symbol = '{market}_{base}'.format(market=market, base=base)
|
|
||||||
|
|
||||||
if (source_dates):
|
|
||||||
start_date = self.get_symbol_start_date(exchange_symbol)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
start_date = cached_symbols[exchange_symbol]['start_date']
|
|
||||||
except KeyError as e:
|
|
||||||
start_date = time.strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
try:
|
|
||||||
end_daily = cached_symbols[exchange_symbol]['end_daily']
|
|
||||||
except KeyError as e:
|
|
||||||
end_daily = 'N/A'
|
|
||||||
|
|
||||||
try:
|
|
||||||
end_minute = cached_symbols[exchange_symbol]['end_minute']
|
|
||||||
except KeyError as e:
|
|
||||||
end_minute = 'N/A'
|
|
||||||
|
|
||||||
symbol_map[exchange_symbol] = dict(
|
|
||||||
symbol=symbol,
|
|
||||||
start_date=start_date,
|
|
||||||
end_daily=end_daily,
|
|
||||||
end_minute=end_minute,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (filename is None):
|
|
||||||
filename = get_exchange_symbols_filename(self.name)
|
|
||||||
|
|
||||||
with open(filename, 'w') as f:
|
|
||||||
json.dump(symbol_map, f, sort_keys=True, indent=2,
|
|
||||||
separators=(',', ':'))
|
|
||||||
|
|
||||||
def get_symbol_start_date(self, symbol):
|
|
||||||
try:
|
|
||||||
r = self.api.returnchartdata(symbol, 86400, pd.to_datetime(
|
|
||||||
'2010-1-1').value // 10 ** 9)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
return time.strftime('%Y-%m-%d', time.gmtime(int(r[0]['date'])))
|
|
||||||
|
|
||||||
def check_open_orders(self):
|
|
||||||
"""
|
|
||||||
Need to override this function for Poloniex:
|
|
||||||
|
|
||||||
Loop through the list of open orders in the Portfolio object.
|
|
||||||
Check if any transactions have been executed:
|
|
||||||
If so, create a transaction and apply to the Portfolio.
|
|
||||||
Check if the order is still open:
|
|
||||||
If not, remove it from open orders
|
|
||||||
|
|
||||||
:return:
|
|
||||||
transactions: Transaction[]
|
|
||||||
"""
|
|
||||||
transactions = list()
|
|
||||||
if self.portfolio.open_orders:
|
|
||||||
for order_id in list(self.portfolio.open_orders):
|
|
||||||
|
|
||||||
order = self._portfolio.open_orders[order_id]
|
|
||||||
log.debug('found open order: {}'.format(order_id))
|
|
||||||
|
|
||||||
try:
|
|
||||||
order_open = self.get_order(order_id)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if (order_open):
|
|
||||||
delta = pd.Timestamp.utcnow() - order.dt
|
|
||||||
log.info(
|
|
||||||
'order {order_id} still open after {delta}'.format(
|
|
||||||
order_id=order_id,
|
|
||||||
delta=delta)
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = self.api.returnordertrades(order_id)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if ('error' in response):
|
|
||||||
if (not order_open):
|
|
||||||
raise OrphanOrderReverseError(order_id=order_id,
|
|
||||||
exchange=self.name)
|
|
||||||
else:
|
|
||||||
for tx in response:
|
|
||||||
"""
|
|
||||||
We maintain a list of dictionaries of transactions that correspond to
|
|
||||||
partially filled orders, indexed by order_id. Every time we query
|
|
||||||
executed transactions from the exchange, we check if we had that
|
|
||||||
transaction for that order already. If not, we process it.
|
|
||||||
|
|
||||||
When an order if fully filled, we flush the dict of transactions
|
|
||||||
associated with that order.
|
|
||||||
"""
|
|
||||||
if (not filter(
|
|
||||||
lambda item: item['order_id'] == tx['tradeID'],
|
|
||||||
self.transactions[order_id])):
|
|
||||||
log.debug(
|
|
||||||
'Got new transaction for order {}: amount {}, price {}'.format(
|
|
||||||
order_id, tx['amount'], tx['rate']))
|
|
||||||
tx['amount'] = float(tx['amount'])
|
|
||||||
if (tx['type'] == 'sell'):
|
|
||||||
tx['amount'] = -tx['amount']
|
|
||||||
transaction = Transaction(
|
|
||||||
asset=order.asset,
|
|
||||||
amount=tx['amount'],
|
|
||||||
dt=pd.to_datetime(tx['date'], utc=True),
|
|
||||||
price=float(tx['rate']),
|
|
||||||
order_id=tx['tradeID'],
|
|
||||||
# it's a misnomer, but keeping it for compatibility
|
|
||||||
commission=float(tx['fee'])
|
|
||||||
)
|
|
||||||
self.transactions[order_id].append(transaction)
|
|
||||||
self.portfolio.execute_transaction(transaction)
|
|
||||||
transactions.append(transaction)
|
|
||||||
|
|
||||||
if (not order_open):
|
|
||||||
"""
|
|
||||||
Since transactions have been executed individually
|
|
||||||
the only thing left to do is remove them from list of open_orders
|
|
||||||
"""
|
|
||||||
del self.portfolio.open_orders[order_id]
|
|
||||||
del self.transactions[order_id]
|
|
||||||
|
|
||||||
return transactions
|
|
||||||
|
|
||||||
def get_orderbook(self, asset, order_type='all'):
|
|
||||||
exchange_symbol = asset.exchange_symbol
|
|
||||||
data = self.api.returnOrderBook(market=exchange_symbol)
|
|
||||||
|
|
||||||
result = dict()
|
|
||||||
for order_type in data:
|
|
||||||
# TODO: filter by type
|
|
||||||
if order_type != 'asks' and order_type != 'bids':
|
|
||||||
continue
|
|
||||||
|
|
||||||
result[order_type] = []
|
|
||||||
for entry in data[order_type]:
|
|
||||||
if len(entry) == 2:
|
|
||||||
result[order_type].append(
|
|
||||||
dict(
|
|
||||||
rate=float(entry[0]),
|
|
||||||
quantity=float(entry[1])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
# Workaround for backwards compatibility
|
|
||||||
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
|
|
||||||
urlopen = urllib.request.urlopen
|
|
||||||
|
|
||||||
|
|
||||||
class Poloniex_api(object):
|
|
||||||
def __init__(self, key, secret):
|
|
||||||
self.key = key
|
|
||||||
self.secret = secret
|
|
||||||
|
|
||||||
self.max_requests_per_second = 6
|
|
||||||
self.request_cpt = dict()
|
|
||||||
|
|
||||||
self.public = ['returnTicker', 'return24Volume', 'returnOrderBook',
|
|
||||||
'returnTradeHistory', 'returnChartData',
|
|
||||||
'returnCurrencies', 'returnLoanOrders']
|
|
||||||
self.trading = ['returnBalances','returnCompleteBalances','returnDepositAddresses',
|
|
||||||
'generateNewAddress','returnDepositsWithdrawals','returnOpenOrders',
|
|
||||||
'returnTradeHistory','returnOrderTrades',
|
|
||||||
'buy', 'sell', 'cancelOrder', 'moveOrder',
|
|
||||||
'withdraw', 'returnFeeInfo','returnAvailableAccountBalances',
|
|
||||||
'returnTradableBalances', 'transferBalance',
|
|
||||||
'returnMarginAccountSummary','marginBuy','marginSell',
|
|
||||||
'getMarginPosition', 'closeMarginPosition','createLoanOffer',
|
|
||||||
'cancelLoanOffer','returnOpenLoanOffers','returnActiveLoans',
|
|
||||||
'returnLendingHistory','toggleAutoRenew']
|
|
||||||
|
|
||||||
def ask_request(self):
|
|
||||||
"""
|
|
||||||
Asks permission to issue a request to the exchange.
|
|
||||||
The primary purpose is to avoid hitting rate limits.
|
|
||||||
|
|
||||||
The application will pause if the maximum requests per minute
|
|
||||||
permitted by the exchange is exceeded.
|
|
||||||
|
|
||||||
:return boolean:
|
|
||||||
|
|
||||||
"""
|
|
||||||
now = time.time()
|
|
||||||
if not self.request_cpt:
|
|
||||||
self.request_cpt = dict()
|
|
||||||
self.request_cpt[now] = 0
|
|
||||||
return True
|
|
||||||
|
|
||||||
cpt_date = self.request_cpt.keys()[0]
|
|
||||||
cpt = self.request_cpt[cpt_date]
|
|
||||||
|
|
||||||
if now > cpt_date + 1:
|
|
||||||
self.request_cpt = dict()
|
|
||||||
self.request_cpt[now] = 0
|
|
||||||
return True
|
|
||||||
|
|
||||||
if cpt >= self.max_requests_per_second:
|
|
||||||
|
|
||||||
log.debug('max requests 6 reached, sleeping for 1 seconds')
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
now = time.time()
|
|
||||||
self.request_cpt = dict()
|
|
||||||
self.request_cpt[now] = 0
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self.request_cpt[cpt_date] += 1
|
|
||||||
|
|
||||||
def query(self, method, req={}):
|
|
||||||
|
|
||||||
if method in self.public:
|
|
||||||
url = 'https://poloniex.com/public?command=' + method + '&' + urllib.parse.urlencode(req)
|
|
||||||
headers = {}
|
|
||||||
post_data = None
|
|
||||||
elif method in self.trading:
|
|
||||||
url = 'https://poloniex.com/tradingApi'
|
|
||||||
req['command'] = method
|
|
||||||
req['nonce'] = int(time.time()*1000)
|
|
||||||
post_data = urllib.parse.urlencode(req)
|
|
||||||
signature = hmac.new(self.secret, post_data, hashlib.sha512).hexdigest()
|
|
||||||
headers = { 'Sign': signature, 'Key': self.key}
|
|
||||||
else:
|
|
||||||
raise ValueError('Method "' + method + '" not found in neither the Public API or Trading API endpoints')
|
|
||||||
|
|
||||||
self.ask_request()
|
|
||||||
req = urllib.request.Request(url, data=post_data, headers=headers)
|
|
||||||
return json.loads(urlopen(req).read())
|
|
||||||
|
|
||||||
def returnticker(self):
|
|
||||||
return self.query('returnTicker', {})
|
|
||||||
|
|
||||||
def return24volume(self):
|
|
||||||
return self.query('return24Volume', {})
|
|
||||||
|
|
||||||
def returnOrderBook(self, market='all'):
|
|
||||||
return self.query('returnOrderBook', {'currencyPair': market})
|
|
||||||
|
|
||||||
def returntradehistory(self, market, start=None, end=None):
|
|
||||||
if(start is not None and end is not None):
|
|
||||||
return self.query('returntradehistory',
|
|
||||||
{'currencyPair': market, 'start': start, 'end': end })
|
|
||||||
else:
|
|
||||||
return self.query('returntradehistory', {'currencyPair': market })
|
|
||||||
|
|
||||||
def returnchartdata(self, market, period, start, end=9999999999):
|
|
||||||
return self.query('returnChartData', {'currencyPair': market, 'period': period,
|
|
||||||
'start': start, 'end': end})
|
|
||||||
|
|
||||||
def returncurrencies(self):
|
|
||||||
return self.query('returnCurrencies', {})
|
|
||||||
|
|
||||||
def returnloadorders(self, market):
|
|
||||||
return self.query('returnLoanOrders', {'currency': market})
|
|
||||||
|
|
||||||
def returnbalances(self):
|
|
||||||
return self.query('returnBalances')
|
|
||||||
|
|
||||||
def returncompletebalances(self, account):
|
|
||||||
if(account):
|
|
||||||
return self.query('returnCompleteBalances', {'account': account})
|
|
||||||
else:
|
|
||||||
return self.query('returnCompleteBalances')
|
|
||||||
|
|
||||||
def returndepositaddresses(self):
|
|
||||||
return self.query('returnDepositAddresses')
|
|
||||||
|
|
||||||
def generatenewaddress(self, currency):
|
|
||||||
return self.query('generateNewAddress', {'currency': currency})
|
|
||||||
|
|
||||||
def returnDepositsWithdrawals(self, start, end):
|
|
||||||
return self.query('returnDepositsWithdrawals', {'start': start, 'end': end})
|
|
||||||
|
|
||||||
def returnopenorders(self, market):
|
|
||||||
return self.query('returnOpenOrders', {'currencyPair': market})
|
|
||||||
|
|
||||||
def returntradehistory(self, market):
|
|
||||||
#TODO: optional start and/or end and limit
|
|
||||||
return self.query('returnTradeHistory', {'currencyPair': market})
|
|
||||||
|
|
||||||
def returnordertrades(self, ordernumber):
|
|
||||||
return self.query('returnOrderTrades', {'orderNumber': ordernumber})
|
|
||||||
|
|
||||||
def buy(self, market, amount, rate, fillorkill=0, immediateorcancel=0, postonly=0):
|
|
||||||
if(fillorkill):
|
|
||||||
return self.query('buy', {'currencyPair': market, 'rate':rate, 'amount': amount,
|
|
||||||
'fillOrKill': fillorkill, })
|
|
||||||
elif(immediateorcancel):
|
|
||||||
return self.query('buy', {'currencyPair': market, 'rate':rate, 'amount': amount,
|
|
||||||
'immediateOrCancel': immediateorcancel, })
|
|
||||||
elif(postonly):
|
|
||||||
return self.query('buy', {'currencyPair': market, 'rate':rate, 'amount': amount,
|
|
||||||
'postOnly': postonly, })
|
|
||||||
else:
|
|
||||||
return self.query('buy', {'currencyPair': market, 'rate':rate, 'amount': amount, })
|
|
||||||
|
|
||||||
def sell(self, market, amount, rate, fillorkill=0, immediateorcancel=0, postonly=0):
|
|
||||||
if(fillorkill):
|
|
||||||
return self.query('sell', {'currencyPair': market, 'rate':rate, 'amount': amount,
|
|
||||||
'fillOrKill': fillorkill, })
|
|
||||||
elif(immediateorcancel):
|
|
||||||
return self.query('sell', {'currencyPair': market, 'rate':rate, 'amount': amount,
|
|
||||||
'immediateOrCancel': immediateorcancel, })
|
|
||||||
elif(postonly):
|
|
||||||
return self.query('sell', {'currencyPair': market, 'rate':rate, 'amount': amount,
|
|
||||||
'postOnly': postonly, })
|
|
||||||
else:
|
|
||||||
return self.query('sell', {'currencyPair': market, 'rate':rate, 'amount': amount, })
|
|
||||||
|
|
||||||
def cancelorder(self, ordernumber):
|
|
||||||
return self.query('cancelOrder', {'orderNumber': ordernumber})
|
|
||||||
|
|
||||||
def withdraw(self, currency, quantity, address):
|
|
||||||
return self.query('withdraw',
|
|
||||||
{'currency': currency, 'amount': quantity,
|
|
||||||
'address': address})
|
|
||||||
|
|
||||||
def returnfeeinfo(self):
|
|
||||||
return self.query('returnFeeInfo')
|
|
||||||
|
|
||||||
@@ -14,15 +14,14 @@
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.gens.sim_engine import (
|
from catalyst.gens.sim_engine import (
|
||||||
BAR,
|
BAR,
|
||||||
SESSION_START,
|
SESSION_START
|
||||||
MINUTE_END,
|
|
||||||
SESSION_END
|
|
||||||
)
|
)
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
log = Logger('ExchangeClock')
|
log = Logger('ExchangeClock', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class SimpleClock(object):
|
class SimpleClock(object):
|
||||||
@@ -31,7 +30,8 @@ class SimpleClock(object):
|
|||||||
This class is a drop-in replacement for
|
This class is a drop-in replacement for
|
||||||
:class:`zipline.gens.sim_engine.MinuteSimulationClock`.
|
:class:`zipline.gens.sim_engine.MinuteSimulationClock`.
|
||||||
|
|
||||||
This is a stripped down version because crypto exchanges run around the clock.
|
This is a stripped down version because crypto exchanges run
|
||||||
|
around the clock.
|
||||||
|
|
||||||
The :param:`time_skew` parameter represents the time difference between
|
The :param:`time_skew` parameter represents the time difference between
|
||||||
the Broker and the live trading machine's clock.
|
the Broker and the live trading machine's clock.
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
|
|
||||||
|
|
||||||
def get_pretty_stats(stats_df, recorded_cols=None, num_rows=10):
|
|
||||||
"""
|
|
||||||
Format and print the last few rows of a statistics DataFrame.
|
|
||||||
See the pyfolio project for the data structure.
|
|
||||||
|
|
||||||
:param stats_df:
|
|
||||||
:param num_rows:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
stats_df.set_index('period_close', drop=True, inplace=True)
|
|
||||||
stats_df.dropna(axis=1, how='all', inplace=True)
|
|
||||||
|
|
||||||
pd.set_option('display.expand_frame_repr', False)
|
|
||||||
pd.set_option('precision', 3)
|
|
||||||
pd.set_option('display.width', 1000)
|
|
||||||
pd.set_option('display.max_colwidth', 1000)
|
|
||||||
|
|
||||||
columns = ['starting_cash', 'ending_cash', 'portfolio_value',
|
|
||||||
'pnl', 'long_exposure', 'short_exposure', 'orders',
|
|
||||||
'transactions', 'positions']
|
|
||||||
|
|
||||||
if recorded_cols is not None:
|
|
||||||
for column in recorded_cols:
|
|
||||||
columns.append(column)
|
|
||||||
|
|
||||||
def format_positions(positions):
|
|
||||||
parts = []
|
|
||||||
for position in positions:
|
|
||||||
msg = '{amount:.2f}{market} cost basis {cost_basis:.4f}{base}'.format(
|
|
||||||
amount=position['amount'],
|
|
||||||
market=position['sid'].market_currency,
|
|
||||||
cost_basis=position['cost_basis'],
|
|
||||||
base=position['sid'].base_currency
|
|
||||||
)
|
|
||||||
parts.append(msg)
|
|
||||||
return ', '.join(parts)
|
|
||||||
|
|
||||||
formatters = {
|
|
||||||
'orders': lambda orders: len(orders),
|
|
||||||
'transactions': lambda transactions: len(transactions),
|
|
||||||
'returns': lambda returns: "{0:.4f}".format(returns),
|
|
||||||
'positions': format_positions
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats_df.tail(num_rows).to_string(
|
|
||||||
columns=columns,
|
|
||||||
formatters=formatters
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst.data.bundles.core import download_without_progress
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst.data.bundles.core import download_without_progress
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
||||||
|
|
||||||
|
EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
|
||||||
|
API_URL = 'http://data.enigma.co/api/v1'
|
||||||
|
|
||||||
|
|
||||||
|
def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
||||||
|
"""
|
||||||
|
Download and extract a bcolz bundle.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
symbol: str
|
||||||
|
data_frequency: str
|
||||||
|
period: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
Filename: bitfinex-daily-neo_eth-2017-10.tar.gz
|
||||||
|
|
||||||
|
"""
|
||||||
|
root = get_exchange_bundles_folder(exchange_name)
|
||||||
|
name = '{exchange}-{frequency}-{symbol}-{period}'.format(
|
||||||
|
exchange=exchange_name,
|
||||||
|
frequency=data_frequency,
|
||||||
|
symbol=symbol,
|
||||||
|
period=period
|
||||||
|
)
|
||||||
|
path = os.path.join(root, name)
|
||||||
|
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
||||||
|
'exchange-{exchange}/{name}.tar.gz'.format(
|
||||||
|
exchange=exchange_name,
|
||||||
|
name=name)
|
||||||
|
|
||||||
|
bytes = download_without_progress(url)
|
||||||
|
with tarfile.open('r', fileobj=bytes) as tar:
|
||||||
|
tar.extractall(path)
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def get_df_from_arrays(arrays, periods):
|
||||||
|
"""
|
||||||
|
A DataFrame from the specified OHCLV arrays.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
arrays: Object
|
||||||
|
periods: DateTimeIndex
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
|
||||||
|
"""
|
||||||
|
ohlcv = dict()
|
||||||
|
for index, field in enumerate(
|
||||||
|
['open', 'high', 'low', 'close', 'volume']):
|
||||||
|
ohlcv[field] = arrays[index].flatten()
|
||||||
|
|
||||||
|
df = pd.DataFrame(
|
||||||
|
data=ohlcv,
|
||||||
|
index=periods
|
||||||
|
)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def range_in_bundle(asset, start_dt, end_dt, reader):
|
||||||
|
"""
|
||||||
|
Evaluate whether price data of an asset is included has been ingested in
|
||||||
|
the exchange bundle for the given date range.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset: TradingPair
|
||||||
|
start_dt: datetime
|
||||||
|
end_dt: datetime
|
||||||
|
reader: BcolzBarMinuteReader
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
has_data = True
|
||||||
|
dates = [start_dt, end_dt]
|
||||||
|
|
||||||
|
while dates and has_data:
|
||||||
|
try:
|
||||||
|
dt = dates.pop(0)
|
||||||
|
close = reader.get_value(asset.sid, dt, 'close')
|
||||||
|
|
||||||
|
if np.isnan(close):
|
||||||
|
has_data = False
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
has_data = False
|
||||||
|
|
||||||
|
return has_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_assets(exchange, include_symbols, exclude_symbols):
|
||||||
|
"""
|
||||||
|
Get assets from an exchange, including or excluding the specified
|
||||||
|
symbols.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange: Exchange
|
||||||
|
include_symbols: str
|
||||||
|
exclude_symbols: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[TradingPair]
|
||||||
|
|
||||||
|
"""
|
||||||
|
if include_symbols is not None:
|
||||||
|
include_symbols_list = include_symbols.split(',')
|
||||||
|
|
||||||
|
return exchange.get_assets(include_symbols_list)
|
||||||
|
|
||||||
|
else:
|
||||||
|
all_assets = exchange.get_assets()
|
||||||
|
|
||||||
|
if exclude_symbols is not None:
|
||||||
|
exclude_symbols_list = exclude_symbols.split(',')
|
||||||
|
|
||||||
|
assets = []
|
||||||
|
for asset in all_assets:
|
||||||
|
if asset.symbol not in exclude_symbols_list:
|
||||||
|
assets.append(asset)
|
||||||
|
|
||||||
|
return assets
|
||||||
|
|
||||||
|
else:
|
||||||
|
return all_assets
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
import calendar
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timedelta, date
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||||
|
InvalidHistoryFrequencyAlias
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_from_ms(ms):
|
||||||
|
"""
|
||||||
|
The date from the number of miliseconds from the epoch.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ms: int
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
return datetime.fromtimestamp(ms / 1000.0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_seconds_from_date(date):
|
||||||
|
"""
|
||||||
|
The number of seconds from the epoch.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
date: datetime
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
|
||||||
|
"""
|
||||||
|
epoch = datetime.utcfromtimestamp(0)
|
||||||
|
epoch = epoch.replace(tzinfo=pytz.UTC)
|
||||||
|
|
||||||
|
return int((date - epoch).total_seconds())
|
||||||
|
|
||||||
|
|
||||||
|
def get_delta(periods, data_frequency):
|
||||||
|
"""
|
||||||
|
Get a time delta based on the specified data frequency.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
periods: int
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
timedelta
|
||||||
|
|
||||||
|
"""
|
||||||
|
return timedelta(minutes=periods) \
|
||||||
|
if data_frequency == 'minute' else timedelta(days=periods)
|
||||||
|
|
||||||
|
|
||||||
|
def get_periods_range(freq, start_dt=None, end_dt=None, periods=None):
|
||||||
|
"""
|
||||||
|
Get a date range for the specified parameters.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
start_dt: datetime
|
||||||
|
end_dt: datetime
|
||||||
|
freq: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DateTimeIndex
|
||||||
|
|
||||||
|
"""
|
||||||
|
if freq == 'minute':
|
||||||
|
freq = 'T'
|
||||||
|
|
||||||
|
elif freq == 'daily':
|
||||||
|
freq = 'D'
|
||||||
|
|
||||||
|
if start_dt is not None and end_dt is not None and periods is None:
|
||||||
|
|
||||||
|
return pd.date_range(start_dt, end_dt, freq=freq)
|
||||||
|
|
||||||
|
elif periods is not None and (start_dt is not None or end_dt is not None):
|
||||||
|
_, unit_periods, unit, _ = get_frequency(freq)
|
||||||
|
adj_periods = periods * unit_periods
|
||||||
|
|
||||||
|
# TODO: standardize time aliases to avoid any mapping
|
||||||
|
unit = 'd' if unit == 'D' else 'h' if unit == 'H' else 'm'
|
||||||
|
delta = pd.Timedelta(adj_periods, unit)
|
||||||
|
|
||||||
|
if start_dt is not None:
|
||||||
|
return pd.date_range(
|
||||||
|
start=start_dt,
|
||||||
|
end=start_dt + delta,
|
||||||
|
freq=freq,
|
||||||
|
closed='left',
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return pd.date_range(
|
||||||
|
start=end_dt - delta,
|
||||||
|
end=end_dt,
|
||||||
|
freq=freq,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Choose only two parameters between start_dt, end_dt '
|
||||||
|
'and periods.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_periods(start_dt, end_dt, freq):
|
||||||
|
"""
|
||||||
|
The number of periods in the specified range.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
start_dt: datetime
|
||||||
|
end_dt: datetime
|
||||||
|
freq: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
|
||||||
|
"""
|
||||||
|
return len(get_periods_range(start_dt=start_dt, end_dt=end_dt, freq=freq))
|
||||||
|
|
||||||
|
|
||||||
|
def get_start_dt(end_dt, bar_count, data_frequency, include_first=True):
|
||||||
|
"""
|
||||||
|
The start date based on specified end date and data frequency.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
end_dt: datetime
|
||||||
|
bar_count: int
|
||||||
|
data_frequency: str
|
||||||
|
include_first
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
periods = bar_count
|
||||||
|
if periods > 1:
|
||||||
|
delta = get_delta(periods, data_frequency)
|
||||||
|
start_dt = end_dt - delta
|
||||||
|
|
||||||
|
if not include_first:
|
||||||
|
start_dt += get_delta(1, data_frequency)
|
||||||
|
else:
|
||||||
|
start_dt = end_dt
|
||||||
|
|
||||||
|
return start_dt
|
||||||
|
|
||||||
|
|
||||||
|
def get_period_label(dt, data_frequency):
|
||||||
|
"""
|
||||||
|
The period label for the specified date and frequency.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dt: datetime
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if data_frequency == 'minute':
|
||||||
|
return '{}-{:02d}'.format(dt.year, dt.month)
|
||||||
|
else:
|
||||||
|
return '{}'.format(dt.year)
|
||||||
|
|
||||||
|
|
||||||
|
def get_month_start_end(dt, first_day=None, last_day=None):
|
||||||
|
"""
|
||||||
|
The first and last day of the month for the specified date.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dt: datetime
|
||||||
|
first_day: datetime
|
||||||
|
last_day: datetime
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
datetime, datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
month_range = calendar.monthrange(dt.year, dt.month)
|
||||||
|
|
||||||
|
if first_day:
|
||||||
|
month_start = first_day
|
||||||
|
else:
|
||||||
|
month_start = pd.to_datetime(datetime(
|
||||||
|
dt.year, dt.month, 1, 0, 0, 0, 0
|
||||||
|
), utc=True)
|
||||||
|
|
||||||
|
if last_day:
|
||||||
|
month_end = last_day
|
||||||
|
else:
|
||||||
|
month_end = pd.to_datetime(datetime(
|
||||||
|
dt.year, dt.month, month_range[1], 23, 59, 0, 0
|
||||||
|
), utc=True)
|
||||||
|
|
||||||
|
if month_end > pd.Timestamp.utcnow():
|
||||||
|
month_end = pd.Timestamp.utcnow().floor('1D')
|
||||||
|
|
||||||
|
return month_start, month_end
|
||||||
|
|
||||||
|
|
||||||
|
def get_year_start_end(dt, first_day=None, last_day=None):
|
||||||
|
"""
|
||||||
|
The first and last day of the year for the specified date.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
|
||||||
|
dt: datetime
|
||||||
|
first_day: datetime
|
||||||
|
last_day: datetime
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
datetime, datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
year_start = first_day if first_day \
|
||||||
|
else pd.to_datetime(date(dt.year, 1, 1), utc=True)
|
||||||
|
year_end = last_day if last_day \
|
||||||
|
else pd.to_datetime(date(dt.year, 12, 31), utc=True)
|
||||||
|
|
||||||
|
if year_end > pd.Timestamp.utcnow():
|
||||||
|
year_end = pd.Timestamp.utcnow().floor('1D')
|
||||||
|
|
||||||
|
return year_start, year_end
|
||||||
|
|
||||||
|
|
||||||
|
def get_frequency(freq, data_frequency=None, supported_freqs=['D', 'H', 'T']):
|
||||||
|
"""
|
||||||
|
Takes an arbitrary candle size (e.g. 15T) and converts to the lowest
|
||||||
|
common denominator supported by the data bundles (e.g. 1T). The data
|
||||||
|
bundles only support 1T and 1D frequencies. If another frequency
|
||||||
|
is requested, Catalyst must request the underlying data and resample.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
We're trying to use Pandas convention for frequency aliases.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
freq: str
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str, int, str, str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if data_frequency is None:
|
||||||
|
data_frequency = 'daily' if freq.upper().endswith('D') else 'minute'
|
||||||
|
|
||||||
|
if freq == 'minute':
|
||||||
|
unit = 'T'
|
||||||
|
candle_size = 1
|
||||||
|
|
||||||
|
elif freq == 'daily':
|
||||||
|
unit = 'D'
|
||||||
|
candle_size = 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I)
|
||||||
|
if freq_match:
|
||||||
|
candle_size = int(freq_match.group(1)) if freq_match.group(1) \
|
||||||
|
else 1
|
||||||
|
unit = freq_match.group(2)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
|
|
||||||
|
# TODO: some exchanges support H and W frequencies but not bundles
|
||||||
|
# Find a way to pass-through these parameters to exchanges
|
||||||
|
# but resample from minute or daily in backtest mode
|
||||||
|
# see catalyst/exchange/ccxt/ccxt_exchange.py:242 for mapping between
|
||||||
|
# Pandas offet aliases (used by Catalyst) and the CCXT timeframes
|
||||||
|
if unit.lower() == 'd':
|
||||||
|
unit = 'D'
|
||||||
|
alias = '{}D'.format(candle_size)
|
||||||
|
|
||||||
|
if data_frequency == 'minute':
|
||||||
|
data_frequency = 'daily'
|
||||||
|
|
||||||
|
elif unit.lower() == 'm' or unit == 'T':
|
||||||
|
unit = 'T'
|
||||||
|
alias = '{}T'.format(candle_size)
|
||||||
|
data_frequency = 'minute'
|
||||||
|
|
||||||
|
elif unit.lower() == 'h':
|
||||||
|
data_frequency = 'minute'
|
||||||
|
|
||||||
|
if 'H' in supported_freqs:
|
||||||
|
unit = 'H'
|
||||||
|
alias = '{}H'.format(candle_size)
|
||||||
|
else:
|
||||||
|
candle_size = candle_size * 60
|
||||||
|
alias = '{}T'.format(candle_size)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidHistoryFrequencyAlias(freq=freq)
|
||||||
|
|
||||||
|
return alias, candle_size, unit, data_frequency
|
||||||
|
|
||||||
|
|
||||||
|
def from_ms_timestamp(ms):
|
||||||
|
return pd.to_datetime(ms, unit='ms', utc=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_epoch():
|
||||||
|
return pd.to_datetime('1970-1-1', utc=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_candles_number_from_minutes(unit, candle_size, minutes):
|
||||||
|
"""
|
||||||
|
Get the number of bars needed for the given time interval
|
||||||
|
in minutes.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
Supports only "T", "D" and "H" units
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
unit: str
|
||||||
|
candle_size : int
|
||||||
|
minutes: int
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
|
||||||
|
"""
|
||||||
|
if unit == "T":
|
||||||
|
res = (float(minutes) / candle_size)
|
||||||
|
elif unit == "H":
|
||||||
|
res = (minutes / 60.0) / candle_size
|
||||||
|
else: # unit == "D"
|
||||||
|
res = (minutes / 1440.0) / candle_size
|
||||||
|
|
||||||
|
return int(math.ceil(res))
|
||||||
@@ -0,0 +1,753 @@
|
|||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import shutil
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from six import string_types
|
||||||
|
from six.moves.urllib import request
|
||||||
|
|
||||||
|
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
|
||||||
|
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound
|
||||||
|
from catalyst.exchange.utils.serialization_utils import ExchangeJSONEncoder, \
|
||||||
|
ExchangeJSONDecoder
|
||||||
|
from catalyst.utils.paths import data_root, ensure_directory, \
|
||||||
|
last_modified_time
|
||||||
|
|
||||||
|
|
||||||
|
def get_sid(symbol):
|
||||||
|
"""
|
||||||
|
Create a sid by hashing the symbol of a currency pair.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
symbol: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
The resulting sid.
|
||||||
|
|
||||||
|
"""
|
||||||
|
sid = int(
|
||||||
|
hashlib.sha256(symbol.encode('utf-8')).hexdigest(), 16
|
||||||
|
) % 10 ** 6
|
||||||
|
return sid
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange_folder(exchange_name, environ=None):
|
||||||
|
"""
|
||||||
|
The root path of an exchange folder.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not environ:
|
||||||
|
environ = os.environ
|
||||||
|
|
||||||
|
root = data_root(environ)
|
||||||
|
exchange_folder = os.path.join(root, 'exchanges', exchange_name)
|
||||||
|
ensure_directory(exchange_folder)
|
||||||
|
|
||||||
|
return exchange_folder
|
||||||
|
|
||||||
|
|
||||||
|
def is_blacklist(exchange_name, environ=None):
|
||||||
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
|
filename = os.path.join(exchange_folder, 'blacklist.txt')
|
||||||
|
|
||||||
|
return os.path.exists(filename)
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange_symbols_filename(exchange_name, is_local=False, environ=None):
|
||||||
|
"""
|
||||||
|
The absolute path of the exchange's symbol.json file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name:
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = 'symbols.json' if not is_local else 'symbols_local.json'
|
||||||
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
|
return os.path.join(exchange_folder, name)
|
||||||
|
|
||||||
|
|
||||||
|
def download_exchange_symbols(exchange_name, environ=None):
|
||||||
|
"""
|
||||||
|
Downloads the exchange's symbols.json from the repository.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
filename = get_exchange_symbols_filename(exchange_name)
|
||||||
|
url = SYMBOLS_URL.format(exchange=exchange_name)
|
||||||
|
response = request.urlretrieve(url=url, filename=filename)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange_symbols(exchange_name, is_local=False, environ=None):
|
||||||
|
"""
|
||||||
|
The de-serialized content of the exchange's symbols.json.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
is_local: bool
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Object
|
||||||
|
|
||||||
|
"""
|
||||||
|
filename = get_exchange_symbols_filename(exchange_name, is_local)
|
||||||
|
|
||||||
|
if not is_local and (not os.path.isfile(filename) or pd.Timedelta(
|
||||||
|
pd.Timestamp('now', tz='UTC') - last_modified_time(
|
||||||
|
filename)).days > 1):
|
||||||
|
try:
|
||||||
|
download_exchange_symbols(exchange_name, environ)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
with open(filename) as data_file:
|
||||||
|
try:
|
||||||
|
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
||||||
|
return data
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return dict()
|
||||||
|
else:
|
||||||
|
raise ExchangeSymbolsNotFound(
|
||||||
|
exchange=exchange_name,
|
||||||
|
filename=filename
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_exchange_symbols(exchange_name, assets, is_local=False, environ=None):
|
||||||
|
"""
|
||||||
|
Save assets into an exchange_symbols file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
assets: list[dict[str, object]]
|
||||||
|
is_local: bool
|
||||||
|
environ
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
asset_dicts = dict()
|
||||||
|
for symbol in assets:
|
||||||
|
asset_dicts[symbol] = assets[symbol].to_dict()
|
||||||
|
|
||||||
|
filename = get_exchange_symbols_filename(
|
||||||
|
exchange_name, is_local, environ
|
||||||
|
)
|
||||||
|
with open(filename, 'wt') as handle:
|
||||||
|
json.dump(asset_dicts, handle, indent=4, default=symbols_serial)
|
||||||
|
|
||||||
|
|
||||||
|
def get_symbols_string(assets):
|
||||||
|
"""
|
||||||
|
A concatenated string of symbols from a list of assets.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
assets: list[TradingPair]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
array = [assets] if isinstance(assets, TradingPair) else assets
|
||||||
|
return ', '.join([asset.symbol for asset in array])
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange_auth(exchange_name, alias=None, environ=None):
|
||||||
|
"""
|
||||||
|
The de-serialized contend of the exchange's auth.json file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Object
|
||||||
|
|
||||||
|
"""
|
||||||
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
|
name = 'auth' if alias is None else alias
|
||||||
|
filename = os.path.join(exchange_folder, '{}.json'.format(name))
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
with open(filename) as data_file:
|
||||||
|
data = json.load(data_file)
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
data = dict(name=exchange_name, key='', secret='')
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(data, f, sort_keys=False, indent=2,
|
||||||
|
separators=(',', ':'))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def delete_algo_folder(algo_name, environ=None):
|
||||||
|
"""
|
||||||
|
Delete the folder containing the algo state.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
|
||||||
|
|
||||||
|
def get_algo_folder(algo_name, environ=None):
|
||||||
|
"""
|
||||||
|
The algorithm root folder of the algorithm.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not environ:
|
||||||
|
environ = os.environ
|
||||||
|
|
||||||
|
root = data_root(environ)
|
||||||
|
algo_folder = os.path.join(root, 'live_algos', algo_name)
|
||||||
|
ensure_directory(algo_folder)
|
||||||
|
|
||||||
|
return algo_folder
|
||||||
|
|
||||||
|
|
||||||
|
def get_algo_object(algo_name, key, environ=None, rel_path=None, how='pickle'):
|
||||||
|
"""
|
||||||
|
The de-serialized object of the algo name and key.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
key: str
|
||||||
|
environ:
|
||||||
|
rel_path: str
|
||||||
|
how: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Object
|
||||||
|
|
||||||
|
"""
|
||||||
|
if algo_name is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
|
||||||
|
if rel_path is not None:
|
||||||
|
folder = os.path.join(folder, rel_path)
|
||||||
|
|
||||||
|
name = '{}.p'.format(key) if how == 'pickle' else '{}.json'.format(key)
|
||||||
|
filename = os.path.join(folder, name)
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
if how == 'pickle':
|
||||||
|
with open(filename, 'rb') as handle:
|
||||||
|
return pickle.load(handle)
|
||||||
|
|
||||||
|
else:
|
||||||
|
with open(filename) as data_file:
|
||||||
|
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
||||||
|
return data
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def save_algo_object(algo_name, key, obj, environ=None, rel_path=None,
|
||||||
|
how='pickle'):
|
||||||
|
"""
|
||||||
|
Serialize and save an object by algo name and key.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
key: str
|
||||||
|
obj: Object
|
||||||
|
environ:
|
||||||
|
rel_path: str
|
||||||
|
how: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
|
||||||
|
if rel_path is not None:
|
||||||
|
folder = os.path.join(folder, rel_path)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
if how == 'json':
|
||||||
|
filename = os.path.join(folder, '{}.json'.format(key))
|
||||||
|
with open(filename, 'wt') as handle:
|
||||||
|
json.dump(obj, handle, indent=4, cls=ExchangeJSONEncoder)
|
||||||
|
|
||||||
|
else:
|
||||||
|
filename = os.path.join(folder, '{}.p'.format(key))
|
||||||
|
with open(filename, 'wb') as handle:
|
||||||
|
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
|
|
||||||
|
|
||||||
|
def get_algo_df(algo_name, key, environ=None, rel_path=None):
|
||||||
|
"""
|
||||||
|
The de-serialized DataFrame of an algo name and key.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
key: str
|
||||||
|
environ:
|
||||||
|
rel_path: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
|
||||||
|
"""
|
||||||
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
|
||||||
|
if rel_path is not None:
|
||||||
|
folder = os.path.join(folder, rel_path)
|
||||||
|
|
||||||
|
filename = os.path.join(folder, key + '.csv')
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
try:
|
||||||
|
with open(filename, 'rb') as handle:
|
||||||
|
return pd.read_csv(handle, index_col=0, parse_dates=True)
|
||||||
|
except IOError:
|
||||||
|
return pd.DataFrame()
|
||||||
|
else:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
|
def save_algo_df(algo_name, key, df, environ=None, rel_path=None):
|
||||||
|
"""
|
||||||
|
Serialize to csv and save a DataFrame by algo name and key.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
key: str
|
||||||
|
df: pd.DataFrame
|
||||||
|
environ:
|
||||||
|
rel_path: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
if rel_path is not None:
|
||||||
|
folder = os.path.join(folder, rel_path)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
filename = os.path.join(folder, key + '.csv')
|
||||||
|
|
||||||
|
with open(filename, 'wt') as handle:
|
||||||
|
df.to_csv(handle, encoding='UTF_8')
|
||||||
|
|
||||||
|
|
||||||
|
def clear_frame_stats_directory(algo_name):
|
||||||
|
"""
|
||||||
|
remove the outdated directory
|
||||||
|
to avoid overloading the disk
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
error: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
error = None
|
||||||
|
algo_folder = get_algo_folder(algo_name)
|
||||||
|
folder = os.path.join(algo_folder, 'frame_stats')
|
||||||
|
if os.path.exists(folder):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
except OSError:
|
||||||
|
error = 'unable to remove {}, the analyze ' \
|
||||||
|
'data will be inconsistent'.format(folder)
|
||||||
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
def remove_old_files(algo_name, today, rel_path, environ=None):
|
||||||
|
"""
|
||||||
|
remove old files from a directory
|
||||||
|
to avoid overloading the disk
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
today: Timestamp
|
||||||
|
rel_path: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
error: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
error = None
|
||||||
|
algo_folder = get_algo_folder(algo_name, environ)
|
||||||
|
folder = os.path.join(algo_folder, rel_path)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
# run on all files in the folder
|
||||||
|
for f in os.listdir(folder):
|
||||||
|
try:
|
||||||
|
file_path = os.path.join(folder, f)
|
||||||
|
creation_unix = os.path.getctime(file_path)
|
||||||
|
creation_time = pd.to_datetime(creation_unix, unit='s', utc=True)
|
||||||
|
|
||||||
|
# if the file is older than 30 days erase it
|
||||||
|
if today - pd.DateOffset(30) > creation_time:
|
||||||
|
os.unlink(file_path)
|
||||||
|
except OSError:
|
||||||
|
error = 'unable to erase files in {}'.format(folder)
|
||||||
|
|
||||||
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange_minute_writer_root(exchange_name, environ=None):
|
||||||
|
"""
|
||||||
|
The minute writer folder for the exchange.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
BcolzExchangeBarWriter
|
||||||
|
|
||||||
|
"""
|
||||||
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
|
|
||||||
|
minute_data_folder = os.path.join(exchange_folder, 'minute_data')
|
||||||
|
ensure_directory(minute_data_folder)
|
||||||
|
|
||||||
|
return minute_data_folder
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange_bundles_folder(exchange_name, environ=None):
|
||||||
|
"""
|
||||||
|
The temp folder for bundle downloads by algo name.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
|
|
||||||
|
temp_bundles = os.path.join(exchange_folder, 'temp_bundles')
|
||||||
|
ensure_directory(temp_bundles)
|
||||||
|
|
||||||
|
return temp_bundles
|
||||||
|
|
||||||
|
|
||||||
|
def has_bundle(exchange_name, data_frequency, environ=None):
|
||||||
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
|
|
||||||
|
folder_name = '{}_bundle'.format(data_frequency.lower())
|
||||||
|
folder = os.path.join(exchange_folder, folder_name)
|
||||||
|
|
||||||
|
return os.path.isdir(folder)
|
||||||
|
|
||||||
|
|
||||||
|
def symbols_serial(obj):
|
||||||
|
"""
|
||||||
|
JSON serializer for objects not serializable by default json code
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
obj: Object
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(obj, (datetime, date)):
|
||||||
|
return obj.floor('1D').strftime(DATE_FORMAT)
|
||||||
|
|
||||||
|
raise TypeError("Type %s not serializable" % type(obj))
|
||||||
|
|
||||||
|
|
||||||
|
def perf_serial(obj):
|
||||||
|
"""
|
||||||
|
JSON serializer for objects not serializable by default json code
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
obj: Object
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(obj, (datetime, date)):
|
||||||
|
return obj.isoformat()
|
||||||
|
|
||||||
|
raise TypeError("Type %s not serializable" % type(obj))
|
||||||
|
|
||||||
|
|
||||||
|
def get_common_assets(exchanges):
|
||||||
|
"""
|
||||||
|
The assets available in all specified exchanges.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchanges: list[Exchange]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[TradingPair]
|
||||||
|
|
||||||
|
"""
|
||||||
|
symbols = []
|
||||||
|
for exchange_name in exchanges:
|
||||||
|
s = [asset.symbol for asset in exchanges[exchange_name].get_assets()]
|
||||||
|
symbols.append(s)
|
||||||
|
|
||||||
|
inter_symbols = set.intersection(*map(set, symbols))
|
||||||
|
|
||||||
|
assets = []
|
||||||
|
for symbol in inter_symbols:
|
||||||
|
for exchange_name in exchanges:
|
||||||
|
asset = exchanges[exchange_name].get_asset(symbol)
|
||||||
|
assets.append(asset)
|
||||||
|
|
||||||
|
return assets
|
||||||
|
|
||||||
|
|
||||||
|
def resample_history_df(df, freq, field, start_dt=None):
|
||||||
|
"""
|
||||||
|
Resample the OHCLV DataFrame using the specified frequency.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
df: DataFrame
|
||||||
|
freq: str
|
||||||
|
field: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
|
||||||
|
"""
|
||||||
|
if field == 'open':
|
||||||
|
agg = 'first'
|
||||||
|
elif field == 'high':
|
||||||
|
agg = 'max'
|
||||||
|
elif field == 'low':
|
||||||
|
agg = 'min'
|
||||||
|
elif field == 'close':
|
||||||
|
agg = 'last'
|
||||||
|
elif field == 'volume':
|
||||||
|
agg = 'sum'
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid field.')
|
||||||
|
|
||||||
|
resampled_df = df.resample(
|
||||||
|
freq, closed='left', label='left'
|
||||||
|
).agg(agg) # type: pd.DataFrame
|
||||||
|
|
||||||
|
# Because the samples are closed left, we get one more candle at
|
||||||
|
# the beginning then the requested number for bars. Removing this
|
||||||
|
# candle to avoid confusion.
|
||||||
|
if start_dt and not resampled_df.empty:
|
||||||
|
resampled_df = resampled_df[resampled_df.index >= start_dt]
|
||||||
|
|
||||||
|
return resampled_df
|
||||||
|
|
||||||
|
|
||||||
|
def mixin_market_params(exchange_name, params, market):
|
||||||
|
"""
|
||||||
|
Applies a CCXT market dict to parameters of TradingPair init.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
params: dict[Object]
|
||||||
|
market: dict[Object]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: make this more externalized / configurable
|
||||||
|
if 'lot' in market:
|
||||||
|
params['min_trade_size'] = market['lot']
|
||||||
|
params['lot'] = market['lot']
|
||||||
|
|
||||||
|
if exchange_name == 'bitfinex':
|
||||||
|
params['maker'] = 0.001
|
||||||
|
params['taker'] = 0.002
|
||||||
|
|
||||||
|
elif 'maker' in market and 'taker' in market and \
|
||||||
|
market['maker'] is not None and market['taker'] is not None:
|
||||||
|
|
||||||
|
params['maker'] = market['maker']
|
||||||
|
params['taker'] = market['taker']
|
||||||
|
|
||||||
|
else:
|
||||||
|
# TODO: default commission, make configurable
|
||||||
|
params['maker'] = 0.0015
|
||||||
|
params['taker'] = 0.0025
|
||||||
|
|
||||||
|
info = market['info'] if 'info' in market else None
|
||||||
|
if info:
|
||||||
|
if 'minimum_order_size' in info:
|
||||||
|
params['min_trade_size'] = float(info['minimum_order_size'])
|
||||||
|
|
||||||
|
if 'lot' not in params:
|
||||||
|
params['lot'] = params['min_trade_size']
|
||||||
|
|
||||||
|
|
||||||
|
def group_assets_by_exchange(assets):
|
||||||
|
exchange_assets = dict()
|
||||||
|
for asset in assets:
|
||||||
|
if asset.exchange not in exchange_assets:
|
||||||
|
exchange_assets[asset.exchange] = list()
|
||||||
|
|
||||||
|
exchange_assets[asset.exchange].append(asset)
|
||||||
|
|
||||||
|
return exchange_assets
|
||||||
|
|
||||||
|
|
||||||
|
def get_catalyst_symbol(market_or_symbol):
|
||||||
|
"""
|
||||||
|
The Catalyst symbol.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
market_or_symbol
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(market_or_symbol, string_types):
|
||||||
|
parts = market_or_symbol.split('/')
|
||||||
|
return '{}_{}'.format(parts[0].lower(), parts[1].lower())
|
||||||
|
|
||||||
|
else:
|
||||||
|
return '{}_{}'.format(
|
||||||
|
market_or_symbol['base'].lower(),
|
||||||
|
market_or_symbol['quote'].lower(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def save_asset_data(folder, df, decimals=8):
|
||||||
|
symbols = df.index.get_level_values('symbol')
|
||||||
|
for symbol in symbols:
|
||||||
|
symbol_df = df.loc[(symbols == symbol)] # Type: pd.DataFrame
|
||||||
|
|
||||||
|
filename = os.path.join(folder, '{}.csv'.format(symbol))
|
||||||
|
if os.path.exists(filename):
|
||||||
|
print_headers = False
|
||||||
|
|
||||||
|
else:
|
||||||
|
print_headers = True
|
||||||
|
|
||||||
|
with open(filename, 'a') as f:
|
||||||
|
symbol_df.to_csv(
|
||||||
|
path_or_buf=f,
|
||||||
|
header=print_headers,
|
||||||
|
float_format='%.{}f'.format(decimals),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def forward_fill_df_if_needed(df, periods):
|
||||||
|
df = df.reindex(periods)
|
||||||
|
# volume should always be 0 (if there were no trades in this interval)
|
||||||
|
df['volume'] = df['volume'].fillna(0.0)
|
||||||
|
# ie pull the last close into this close
|
||||||
|
df['close'] = df.fillna(method='pad')
|
||||||
|
# now copy the close that was pulled down from the last timestep
|
||||||
|
# into this row, across into o/h/l
|
||||||
|
df['open'] = df['open'].fillna(df['close'])
|
||||||
|
df['low'] = df['low'].fillna(df['close'])
|
||||||
|
df['high'] = df['high'].fillna(df['close'])
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def transform_candles_to_df(candles):
|
||||||
|
return pd.DataFrame(candles).set_index('last_traded')
|
||||||
|
|
||||||
|
|
||||||
|
def get_candles_df(candles, field, freq, bar_count, end_dt):
|
||||||
|
all_series = dict()
|
||||||
|
|
||||||
|
for asset in candles:
|
||||||
|
asset_df = transform_candles_to_df(candles[asset])
|
||||||
|
rounded_end_dt = end_dt.floor(freq)
|
||||||
|
periods = pd.date_range(end=rounded_end_dt,
|
||||||
|
periods=bar_count,
|
||||||
|
freq=freq)
|
||||||
|
asset_df = forward_fill_df_if_needed(asset_df, periods)
|
||||||
|
|
||||||
|
all_series[asset] = pd.Series(asset_df[field])
|
||||||
|
|
||||||
|
df = pd.DataFrame(all_series)
|
||||||
|
df.dropna(inplace=True)
|
||||||
|
|
||||||
|
return df
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
||||||
|
from catalyst.exchange.exchange import Exchange
|
||||||
|
from catalyst.exchange.exchange_errors import ExchangeAuthEmpty
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_exchange_auth, \
|
||||||
|
get_exchange_folder, is_blacklist
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
log = Logger('factory', level=LOG_LEVEL)
|
||||||
|
exchange_cache = dict()
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
||||||
|
skip_init=False, auth_alias=None):
|
||||||
|
key = (exchange_name, base_currency)
|
||||||
|
if key in exchange_cache:
|
||||||
|
return exchange_cache[key]
|
||||||
|
|
||||||
|
exchange_auth = get_exchange_auth(exchange_name, alias=auth_alias)
|
||||||
|
|
||||||
|
has_auth = (exchange_auth['key'] != '' and exchange_auth['secret'] != '')
|
||||||
|
if must_authenticate and not has_auth:
|
||||||
|
raise ExchangeAuthEmpty(
|
||||||
|
exchange=exchange_name.title(),
|
||||||
|
filename=os.path.join(
|
||||||
|
get_exchange_folder(exchange_name), 'auth.json'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
exchange = CCXT(
|
||||||
|
exchange_name=exchange_name,
|
||||||
|
key=exchange_auth['key'],
|
||||||
|
secret=exchange_auth['secret'],
|
||||||
|
password=exchange_auth['password'] if 'password'
|
||||||
|
in exchange_auth.keys() else '',
|
||||||
|
base_currency=base_currency,
|
||||||
|
)
|
||||||
|
exchange_cache[key] = exchange
|
||||||
|
|
||||||
|
if not skip_init:
|
||||||
|
exchange.init()
|
||||||
|
|
||||||
|
return exchange
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchanges(exchange_names):
|
||||||
|
exchanges = dict()
|
||||||
|
for exchange_name in exchange_names:
|
||||||
|
exchanges[exchange_name] = get_exchange(exchange_name)
|
||||||
|
|
||||||
|
return exchanges
|
||||||
|
|
||||||
|
|
||||||
|
def find_exchanges(features=None, skip_blacklist=True, is_authenticated=False,
|
||||||
|
base_currency=None):
|
||||||
|
"""
|
||||||
|
Find exchanges filtered by a list of feature.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
features: str
|
||||||
|
The list of features.
|
||||||
|
|
||||||
|
skip_blacklist: bool
|
||||||
|
is_authenticated: bool
|
||||||
|
base_currency: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[Exchange]
|
||||||
|
|
||||||
|
"""
|
||||||
|
exchange_names = CCXT.find_exchanges(features, is_authenticated)
|
||||||
|
|
||||||
|
exchanges = []
|
||||||
|
for exchange_name in exchange_names:
|
||||||
|
if skip_blacklist and is_blacklist(exchange_name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
exchange = get_exchange(
|
||||||
|
exchange_name=exchange_name,
|
||||||
|
skip_init=True,
|
||||||
|
base_currency=base_currency,
|
||||||
|
)
|
||||||
|
|
||||||
|
if features is not None:
|
||||||
|
if 'dailyBundle' in features \
|
||||||
|
and not exchange.has_bundle('daily'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif 'minuteBundle' in features \
|
||||||
|
and not exchange.has_bundle('minute'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
exchanges.append(exchange)
|
||||||
|
|
||||||
|
return exchanges
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import matplotlib.dates as mdates
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst.exchange.exchange_errors import \
|
||||||
|
MismatchingBaseCurrenciesExchanges
|
||||||
|
|
||||||
|
fmt = mdates.DateFormatter('%Y-%m-%d %H:%M')
|
||||||
|
|
||||||
|
|
||||||
|
def format_ax(ax):
|
||||||
|
"""
|
||||||
|
Trying to assign reasonable parameters to the time axis.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ax:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: room for improvement
|
||||||
|
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
|
||||||
|
ax.xaxis.set_major_formatter(fmt)
|
||||||
|
|
||||||
|
locator = mdates.HourLocator(interval=4)
|
||||||
|
locator.MAXTICKS = 5000
|
||||||
|
ax.xaxis.set_minor_locator(locator)
|
||||||
|
|
||||||
|
datemin = pd.Timestamp.utcnow()
|
||||||
|
ax.set_xlim(datemin)
|
||||||
|
|
||||||
|
ax.grid(True)
|
||||||
|
|
||||||
|
|
||||||
|
def set_legend(ax):
|
||||||
|
"""
|
||||||
|
Set legend on the chart.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ax
|
||||||
|
|
||||||
|
"""
|
||||||
|
ax.legend(loc='upper left', ncol=1, fontsize=10, numpoints=1)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_pnl(ax, df):
|
||||||
|
"""
|
||||||
|
Draw p&l line on the chart.
|
||||||
|
|
||||||
|
"""
|
||||||
|
ax.clear()
|
||||||
|
ax.set_title('Performance')
|
||||||
|
index = df.index.unique()
|
||||||
|
dt = index.get_level_values(level=0)
|
||||||
|
pnl = index.get_level_values(level=4)
|
||||||
|
ax.plot(
|
||||||
|
dt, pnl, '-',
|
||||||
|
color='green',
|
||||||
|
linewidth=1.0,
|
||||||
|
label='Performance'
|
||||||
|
)
|
||||||
|
|
||||||
|
def perc(val):
|
||||||
|
return '{:2f}'.format(val)
|
||||||
|
|
||||||
|
ax.format_ydata = perc
|
||||||
|
|
||||||
|
set_legend(ax)
|
||||||
|
format_ax(ax)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_custom_signals(ax, df):
|
||||||
|
"""
|
||||||
|
Draw custom signals on the chart.
|
||||||
|
|
||||||
|
"""
|
||||||
|
colors = ['blue', 'green', 'red', 'black', 'orange', 'yellow', 'pink']
|
||||||
|
|
||||||
|
ax.clear()
|
||||||
|
ax.set_title('Custom Signals')
|
||||||
|
for index, column in enumerate(df.columns.values.tolist()):
|
||||||
|
ax.plot(df.index, df[column], '-',
|
||||||
|
color=colors[index],
|
||||||
|
linewidth=1.0,
|
||||||
|
label=column
|
||||||
|
)
|
||||||
|
|
||||||
|
set_legend(ax)
|
||||||
|
format_ax(ax)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_exposure(ax, df, context):
|
||||||
|
"""
|
||||||
|
Draw exposure line on the chart.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: list exchanges in graph
|
||||||
|
base_currency = None
|
||||||
|
positions = []
|
||||||
|
for exchange_name in context.exchanges:
|
||||||
|
exchange = context.exchanges[exchange_name]
|
||||||
|
|
||||||
|
if not base_currency:
|
||||||
|
base_currency = exchange.base_currency
|
||||||
|
elif base_currency != exchange.base_currency:
|
||||||
|
raise MismatchingBaseCurrenciesExchanges(
|
||||||
|
base_currency=base_currency,
|
||||||
|
exchange_name=exchange.name,
|
||||||
|
exchange_currency=exchange.base_currency
|
||||||
|
)
|
||||||
|
|
||||||
|
positions += exchange.portfolio.positions
|
||||||
|
|
||||||
|
ax.clear()
|
||||||
|
ax.set_title('Exposure')
|
||||||
|
ax.plot(df.index, df['base_currency'], '-',
|
||||||
|
color='green',
|
||||||
|
linewidth=1.0,
|
||||||
|
label='Base Currency: {}'.format(base_currency.upper())
|
||||||
|
)
|
||||||
|
|
||||||
|
symbols = []
|
||||||
|
for position in positions:
|
||||||
|
symbols.append(position.symbol)
|
||||||
|
|
||||||
|
ax.plot(df.index, df['long_exposure'], '-',
|
||||||
|
color='blue',
|
||||||
|
linewidth=1.0,
|
||||||
|
label='Long Exposure: {}'.format(', '.join(symbols).upper()))
|
||||||
|
|
||||||
|
set_legend(ax)
|
||||||
|
format_ax(ax)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
from json import JSONEncoder
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.constants import DATE_TIME_FORMAT
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeJSONEncoder(json.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, pd.Timestamp):
|
||||||
|
return obj.strftime(DATE_TIME_FORMAT)
|
||||||
|
|
||||||
|
# Let the base class default method raise the TypeError
|
||||||
|
return JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeJSONDecoder(json.JSONDecoder):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
json.JSONDecoder.__init__(
|
||||||
|
self, object_hook=self.object_hook, *args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def recursive_iter(self, obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
for key, value in obj.items():
|
||||||
|
match = isinstance(value, string_types) and re.search(
|
||||||
|
r'(\d{4}-\d{2}-\d{2}).*', value
|
||||||
|
)
|
||||||
|
if match:
|
||||||
|
try:
|
||||||
|
obj[key] = pd.to_datetime(value, utc=True)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif any(isinstance(obj, t) for t in (list, tuple)):
|
||||||
|
for item in obj:
|
||||||
|
self.recursive_iter(item)
|
||||||
|
|
||||||
|
def object_hook(self, obj):
|
||||||
|
self.recursive_iter(obj)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def portfolio_to_dict(portfolio):
|
||||||
|
positions = []
|
||||||
|
for asset in portfolio.positions:
|
||||||
|
p = portfolio.positions[asset] # Type: Position
|
||||||
|
|
||||||
|
position = dict(
|
||||||
|
symbol=asset.symbol,
|
||||||
|
exchange=asset.exchange,
|
||||||
|
amount=p.amount,
|
||||||
|
cost_basis=p.cost_basis,
|
||||||
|
last_sale_price=p.last_sale_price,
|
||||||
|
last_sale_date=p.last_sale_date,
|
||||||
|
)
|
||||||
|
positions.append(position)
|
||||||
|
|
||||||
|
portfolio_dict = vars(portfolio)
|
||||||
|
portfolio_dict['positions'] = positions
|
||||||
|
|
||||||
|
return portfolio_dict
|
||||||
|
|
||||||
|
|
||||||
|
def portfolio_from_dict(self, portfolio_data):
|
||||||
|
from catalyst.protocol import Portfolio
|
||||||
|
return Portfolio()
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
import copy
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import numbers
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_algo_folder
|
||||||
|
from catalyst.utils.paths import data_root, ensure_directory
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
|
s3_conn = []
|
||||||
|
mailgun = []
|
||||||
|
|
||||||
|
|
||||||
|
def trend_direction(series):
|
||||||
|
if series[-1] is np.nan or series[-1] is np.nan:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if series[-1] > series[-2]:
|
||||||
|
return 'up'
|
||||||
|
else:
|
||||||
|
return 'down'
|
||||||
|
|
||||||
|
|
||||||
|
def crossover(source, target):
|
||||||
|
"""
|
||||||
|
The `x`-series is defined as having crossed over `y`-series if the value
|
||||||
|
of `x` is greater than the value of `y` and the value of `x` was less than
|
||||||
|
the value of `y` on the bar immediately preceding the current bar.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
source: Series
|
||||||
|
target: Series
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(target, numbers.Number):
|
||||||
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
|
or target is np.nan:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if source[-1] >= target > source[-2]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
|
or target[-1] is np.nan or target[-2] is np.nan:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if source[-1] > target[-1] and source[-2] < target[-2]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def crossunder(source, target):
|
||||||
|
"""
|
||||||
|
The `x`-series is defined as having crossed under `y`-series if the value
|
||||||
|
of `x` is less than the value of `y` and the value of `x` was greater than
|
||||||
|
the value of `y` on the bar immediately preceding the current bar.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
source: Series
|
||||||
|
target: Series
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(target, numbers.Number):
|
||||||
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
|
or target is np.nan:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if source[-1] < target <= source[-2]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
|
or target[-1] is np.nan or target[-2] is np.nan:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if source[-1] < target[-1] and source[-2] >= target[-2]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def vwap(df):
|
||||||
|
"""
|
||||||
|
Volume-weighted average price (VWAP) is a ratio generally used by
|
||||||
|
institutional investors and mutual funds to make buys and sells so as not
|
||||||
|
to disturb the market prices with large orders. It is the average share
|
||||||
|
price of a stock weighted against its trading volume within a particular
|
||||||
|
time frame, generally one day.
|
||||||
|
|
||||||
|
Read more: Volume Weighted Average Price - VWAP
|
||||||
|
https://www.investopedia.com/terms/v/vwap.asp#ixzz4xt922daE
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
df: pd.DataFrame
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
if 'close' not in df.columns or 'volume' not in df.columns:
|
||||||
|
raise ValueError('price data must include `volume` and `close`')
|
||||||
|
|
||||||
|
vol_sum = np.nansum(df['volume'].values)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ret = np.nansum(df['close'].values * df['volume'].values) / vol_sum
|
||||||
|
except ZeroDivisionError:
|
||||||
|
ret = np.nan
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def set_position_row(row, asset, asset_values=list()):
|
||||||
|
"""
|
||||||
|
Apply the position data as individual columns.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
row: dict[str, Object]
|
||||||
|
asset: TradingPair
|
||||||
|
asset_values: list[str]
|
||||||
|
If a recorded_col contains a tuple which first value is an asset
|
||||||
|
matching a position, its value will be displayed with the
|
||||||
|
position and not in the index.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
asset_cols = ['symbol']
|
||||||
|
row['symbol'] = asset.symbol
|
||||||
|
|
||||||
|
position = next((p for p in row['positions'] if p['sid'] == asset), None)
|
||||||
|
|
||||||
|
columns = ['amount', 'cost_basis', 'last_sale_price']
|
||||||
|
for column in columns:
|
||||||
|
if position is not None:
|
||||||
|
row[column] = position[column]
|
||||||
|
|
||||||
|
else:
|
||||||
|
row[column] = 0
|
||||||
|
|
||||||
|
asset_cols.append(column)
|
||||||
|
|
||||||
|
values = asset_values[asset] if asset in asset_values else list()
|
||||||
|
for column in values:
|
||||||
|
row[column] = values[column]
|
||||||
|
|
||||||
|
asset_cols.append(column)
|
||||||
|
|
||||||
|
return asset_cols
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_stats(stats, recorded_cols=list()):
|
||||||
|
"""
|
||||||
|
Prepare the stats DataFrame for user-friendly output.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
stats: list[Object]
|
||||||
|
recorded_cols: list[str]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
asset_cols = list()
|
||||||
|
|
||||||
|
stats = copy.deepcopy(stats)
|
||||||
|
# Using a copy since we are adding rows inside the loop.
|
||||||
|
for row_index, row_data in enumerate(list(stats)):
|
||||||
|
assets = [p['sid'] for p in row_data['positions']]
|
||||||
|
|
||||||
|
asset_values = dict()
|
||||||
|
if recorded_cols is not None:
|
||||||
|
for column in recorded_cols[:]:
|
||||||
|
value = row_data[column]
|
||||||
|
if isinstance(value, pd.Series):
|
||||||
|
value = value.to_dict()
|
||||||
|
|
||||||
|
if type(value) is dict:
|
||||||
|
for asset in value:
|
||||||
|
if not isinstance(asset, TradingPair):
|
||||||
|
break
|
||||||
|
|
||||||
|
if asset not in assets:
|
||||||
|
assets.append(asset)
|
||||||
|
|
||||||
|
if asset not in asset_values:
|
||||||
|
asset_values[asset] = dict()
|
||||||
|
|
||||||
|
asset_values[asset][column] = value[asset]
|
||||||
|
|
||||||
|
if len(assets) == 1:
|
||||||
|
row = stats[row_index]
|
||||||
|
asset_cols = set_position_row(row, assets[0], asset_values)
|
||||||
|
|
||||||
|
elif len(assets) > 1:
|
||||||
|
for asset_index, asset in enumerate(assets):
|
||||||
|
if asset_index > 0:
|
||||||
|
row = copy.deepcopy(row_data)
|
||||||
|
stats.append(row)
|
||||||
|
|
||||||
|
else:
|
||||||
|
row = stats[row_index]
|
||||||
|
|
||||||
|
asset_cols = set_position_row(row, assets[asset_index],
|
||||||
|
asset_values)
|
||||||
|
|
||||||
|
df = pd.DataFrame(stats)
|
||||||
|
df['orders'] = df['orders'].apply(lambda orders: len(orders))
|
||||||
|
df['transactions'] = df['transactions'].apply(
|
||||||
|
lambda transactions: len(transactions)
|
||||||
|
)
|
||||||
|
index_cols = [
|
||||||
|
'period_close', 'starting_cash', 'ending_cash', 'portfolio_value',
|
||||||
|
'pnl', 'long_exposure', 'short_exposure', 'orders', 'transactions',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Removing the asset specific entries
|
||||||
|
if recorded_cols is not None:
|
||||||
|
recorded_cols = [x for x in recorded_cols if x not in asset_cols]
|
||||||
|
for column in recorded_cols:
|
||||||
|
index_cols.append(column)
|
||||||
|
|
||||||
|
if asset_cols:
|
||||||
|
columns = asset_cols
|
||||||
|
df.set_index(index_cols, drop=True, inplace=True)
|
||||||
|
|
||||||
|
else:
|
||||||
|
columns = index_cols
|
||||||
|
columns.remove('period_close')
|
||||||
|
df.set_index('period_close', drop=False, inplace=True)
|
||||||
|
|
||||||
|
df.dropna(axis=1, how='all', inplace=True)
|
||||||
|
df.sort_index(axis=0, level=0, inplace=True)
|
||||||
|
|
||||||
|
return df, columns
|
||||||
|
|
||||||
|
|
||||||
|
def set_print_settings():
|
||||||
|
pd.set_option('display.expand_frame_repr', False)
|
||||||
|
pd.set_option('precision', 8)
|
||||||
|
pd.set_option('display.width', 1000)
|
||||||
|
pd.set_option('display.max_colwidth', 1000)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pretty_stats(stats, recorded_cols=None, num_rows=10, show_tail=True):
|
||||||
|
"""
|
||||||
|
Format and print the last few rows of a statistics DataFrame.
|
||||||
|
See the pyfolio project for the data structure.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
stats: list[Object]
|
||||||
|
An array of statistics for the period.
|
||||||
|
|
||||||
|
num_rows: int
|
||||||
|
The number of rows to display on the screen.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(stats, pd.DataFrame):
|
||||||
|
stats = list(stats.T.to_dict().values())
|
||||||
|
stats.sort(key=itemgetter('period_close'))
|
||||||
|
|
||||||
|
if len(stats) > num_rows:
|
||||||
|
display_stats = stats[-num_rows:] if show_tail else stats[0:num_rows]
|
||||||
|
else:
|
||||||
|
display_stats = stats
|
||||||
|
|
||||||
|
df, columns = prepare_stats(
|
||||||
|
display_stats, recorded_cols=recorded_cols
|
||||||
|
)
|
||||||
|
set_print_settings()
|
||||||
|
return df.to_string(columns=columns)
|
||||||
|
|
||||||
|
|
||||||
|
def get_csv_stats(stats, recorded_cols=None):
|
||||||
|
"""
|
||||||
|
Create a CSV buffer from the stats DataFrame.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
stats: list[Object]
|
||||||
|
recorded_cols: list[str]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
df, columns = prepare_stats(stats, recorded_cols=recorded_cols)
|
||||||
|
|
||||||
|
return df.to_csv(
|
||||||
|
None,
|
||||||
|
columns=columns,
|
||||||
|
# encoding='utf-8',
|
||||||
|
quoting=csv.QUOTE_NONNUMERIC
|
||||||
|
).encode()
|
||||||
|
|
||||||
|
|
||||||
|
def stats_to_s3(uri, stats, algo_namespace, recorded_cols=None,
|
||||||
|
folder='catalyst/stats', bytes_to_write=None):
|
||||||
|
"""
|
||||||
|
Uploads the performance stats to a S3 bucket.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
uri: str
|
||||||
|
stats: list[Object]
|
||||||
|
algo_namespace: str
|
||||||
|
recorded_cols: list[str]
|
||||||
|
folder: str
|
||||||
|
bytes_to_write: str
|
||||||
|
Option to reuse bytes instead of re-computing the csv
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not s3_conn:
|
||||||
|
import boto3
|
||||||
|
s3_conn.append(boto3.resource('s3'))
|
||||||
|
|
||||||
|
s3 = s3_conn[0]
|
||||||
|
|
||||||
|
if bytes_to_write is None:
|
||||||
|
bytes_to_write = get_csv_stats(stats, recorded_cols=recorded_cols)
|
||||||
|
|
||||||
|
now = pd.Timestamp.utcnow()
|
||||||
|
timestr = now.strftime('%Y%m%d')
|
||||||
|
pid = os.getpid()
|
||||||
|
|
||||||
|
parts = uri.split('//')
|
||||||
|
path = '{folder}/{algo}/{time}-{algo}-{pid}.csv'.format(
|
||||||
|
folder=folder,
|
||||||
|
algo=algo_namespace,
|
||||||
|
time=timestr,
|
||||||
|
pid=pid,
|
||||||
|
)
|
||||||
|
obj = s3.Object(parts[1], path)
|
||||||
|
obj.put(Body=bytes_to_write)
|
||||||
|
|
||||||
|
|
||||||
|
def email_error(algo_name, dt, e, environ=None):
|
||||||
|
import requests
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
if not mailgun:
|
||||||
|
root = data_root(environ)
|
||||||
|
filename = os.path.join(root, 'mailgun.json')
|
||||||
|
if not os.path.exists(filename):
|
||||||
|
raise ValueError(
|
||||||
|
'mailgun.json not found in the catalyst data folder'
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(filename) as data_file:
|
||||||
|
mailgun.append(json.load(data_file))
|
||||||
|
|
||||||
|
mg = mailgun[0]
|
||||||
|
|
||||||
|
return requests.post(
|
||||||
|
mg['url'],
|
||||||
|
auth=("api", mg['api']),
|
||||||
|
data={
|
||||||
|
"from": mg['from'],
|
||||||
|
"to": mg['to'],
|
||||||
|
"subject": 'Error: {}'.format(algo_name),
|
||||||
|
"text": '{}\n\n{}\n{}'.format(
|
||||||
|
dt, e, traceback.format_exc()
|
||||||
|
)})
|
||||||
|
|
||||||
|
|
||||||
|
def stats_to_algo_folder(stats, algo_namespace,
|
||||||
|
folder_name, recorded_cols=None):
|
||||||
|
"""
|
||||||
|
Saves the performance stats to the algo local folder.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
stats: list[Object]
|
||||||
|
algo_namespace: str
|
||||||
|
folder_name: str
|
||||||
|
recorded_cols: list[str]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
bytes_to_write = get_csv_stats(stats, recorded_cols=recorded_cols)
|
||||||
|
|
||||||
|
timestr = time.strftime('%Y%m%d')
|
||||||
|
folder = get_algo_folder(algo_namespace)
|
||||||
|
|
||||||
|
stats_folder = os.path.join(folder, folder_name)
|
||||||
|
ensure_directory(stats_folder)
|
||||||
|
|
||||||
|
filename = os.path.join(stats_folder, '{}.csv'.format(timestr))
|
||||||
|
|
||||||
|
with open(filename, 'wb') as handle:
|
||||||
|
handle.write(bytes_to_write)
|
||||||
|
|
||||||
|
return bytes_to_write
|
||||||
|
|
||||||
|
|
||||||
|
def df_to_string(df):
|
||||||
|
"""
|
||||||
|
Create a formatted str representation of the DataFrame.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
df: DataFrame
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
pd.set_option('display.expand_frame_repr', False)
|
||||||
|
pd.set_option('precision', 8)
|
||||||
|
pd.set_option('display.width', 1000)
|
||||||
|
pd.set_option('display.max_colwidth', 1000)
|
||||||
|
|
||||||
|
return df.to_string()
|
||||||
|
|
||||||
|
|
||||||
|
def extract_orders(perf):
|
||||||
|
order_list = perf.orders.values
|
||||||
|
all_orders = [t for sublist in order_list for t in sublist]
|
||||||
|
all_orders.sort(key=lambda o: o['dt'])
|
||||||
|
|
||||||
|
orders = pd.DataFrame(all_orders)
|
||||||
|
if not orders.empty:
|
||||||
|
orders.set_index('dt', inplace=True, drop=True)
|
||||||
|
return orders
|
||||||
|
|
||||||
|
|
||||||
|
def extract_transactions(perf):
|
||||||
|
"""
|
||||||
|
Compute indexes for buy and sell transactions
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
perf: DataFrame
|
||||||
|
The algo performance DataFrame.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
A DataFrame of transactions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
trans_list = perf.transactions.values
|
||||||
|
all_trans = [t for sublist in trans_list for t in sublist]
|
||||||
|
all_trans.sort(key=lambda t: t['dt'])
|
||||||
|
|
||||||
|
transactions = pd.DataFrame(all_trans)
|
||||||
|
if not transactions.empty:
|
||||||
|
transactions.set_index('dt', inplace=True, drop=True)
|
||||||
|
return transactions
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import os
|
||||||
|
import random
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
||||||
|
from catalyst.exchange.utils.factory import find_exchanges
|
||||||
|
from catalyst.utils.paths import ensure_directory
|
||||||
|
|
||||||
|
|
||||||
|
def handle_exchange_error(exchange, e):
|
||||||
|
try:
|
||||||
|
message = '{}: {}'.format(
|
||||||
|
e.__class__, e.message.decode('ascii', 'ignore')
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
message = 'unexpected error'
|
||||||
|
|
||||||
|
folder = get_exchange_folder(exchange.name)
|
||||||
|
filename = os.path.join(folder, 'blacklist.txt')
|
||||||
|
with open(filename, 'wt') as handle:
|
||||||
|
handle.write(message)
|
||||||
|
|
||||||
|
|
||||||
|
def select_random_exchanges(population=3, features=None,
|
||||||
|
is_authenticated=False, base_currency=None):
|
||||||
|
all_exchanges = find_exchanges(
|
||||||
|
features=features,
|
||||||
|
is_authenticated=is_authenticated,
|
||||||
|
base_currency=base_currency,
|
||||||
|
)
|
||||||
|
|
||||||
|
if population is not None:
|
||||||
|
if len(all_exchanges) < population:
|
||||||
|
population = len(all_exchanges)
|
||||||
|
|
||||||
|
exchanges = random.sample(all_exchanges, population)
|
||||||
|
|
||||||
|
else:
|
||||||
|
exchanges = all_exchanges
|
||||||
|
|
||||||
|
return exchanges
|
||||||
|
|
||||||
|
|
||||||
|
def select_random_assets(all_assets, population=3):
|
||||||
|
assets = random.sample(all_assets, population)
|
||||||
|
return assets
|
||||||
|
|
||||||
|
|
||||||
|
def output_df(df, assets, name=None):
|
||||||
|
"""
|
||||||
|
Outputs a price DataFrame to a temp folder.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
df: pd.DataFrame
|
||||||
|
assets
|
||||||
|
name
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
if isinstance(assets, TradingPair):
|
||||||
|
asset_folder = '{}_{}'.format(assets.exchange, assets.symbol)
|
||||||
|
else:
|
||||||
|
asset_folder = ','.join(
|
||||||
|
['{}_{}'.format(a.exchange, a.symbol) for a in assets]
|
||||||
|
)
|
||||||
|
|
||||||
|
folder = os.path.join(
|
||||||
|
tempfile.gettempdir(), 'catalyst', asset_folder
|
||||||
|
)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
name = 'output'
|
||||||
|
|
||||||
|
path = os.path.join(folder, '{}.csv'.format(name))
|
||||||
|
df.to_csv(path)
|
||||||
|
|
||||||
|
return path, folder
|
||||||
@@ -34,7 +34,9 @@ from catalyst.finance.commission import (
|
|||||||
from catalyst.finance.cancel_policy import NeverCancel
|
from catalyst.finance.cancel_policy import NeverCancel
|
||||||
from catalyst.utils.input_validation import expect_types
|
from catalyst.utils.input_validation import expect_types
|
||||||
|
|
||||||
log = Logger('Blotter')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('Blotter', level=LOG_LEVEL)
|
||||||
warning_logger = Logger('AlgoWarning')
|
warning_logger = Logger('AlgoWarning')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ from catalyst.errors import (
|
|||||||
TradingControlViolation,
|
TradingControlViolation,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = logbook.Logger('TradingControl')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('TradingControl', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class TradingControl(with_metaclass(abc.ABCMeta)):
|
class TradingControl(with_metaclass(abc.ABCMeta)):
|
||||||
|
|||||||
@@ -15,13 +15,8 @@
|
|||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
|
||||||
from sys import float_info
|
|
||||||
|
|
||||||
from six import with_metaclass
|
|
||||||
|
|
||||||
import catalyst.utils.math_utils as zp_math
|
|
||||||
|
|
||||||
from numpy import isfinite
|
from numpy import isfinite
|
||||||
|
from six import with_metaclass
|
||||||
|
|
||||||
from catalyst.errors import BadOrderParameters
|
from catalyst.errors import BadOrderParameters
|
||||||
|
|
||||||
@@ -77,6 +72,7 @@ class LimitOrder(ExecutionStyle):
|
|||||||
Execution style representing an order to be executed at a price equal to or
|
Execution style representing an order to be executed at a price equal to or
|
||||||
better than a specified limit price.
|
better than a specified limit price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, limit_price, exchange=None):
|
def __init__(self, limit_price, exchange=None):
|
||||||
"""
|
"""
|
||||||
Store the given price.
|
Store the given price.
|
||||||
@@ -99,6 +95,7 @@ class StopOrder(ExecutionStyle):
|
|||||||
Execution style representing an order to be placed once the market price
|
Execution style representing an order to be placed once the market price
|
||||||
reaches a specified stop price.
|
reaches a specified stop price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, stop_price, exchange=None):
|
def __init__(self, stop_price, exchange=None):
|
||||||
"""
|
"""
|
||||||
Store the given price.
|
Store the given price.
|
||||||
@@ -121,6 +118,7 @@ class StopLimitOrder(ExecutionStyle):
|
|||||||
Execution style representing a limit order to be placed with a specified
|
Execution style representing a limit order to be placed with a specified
|
||||||
limit price once the market reaches a specified stop price.
|
limit price once the market reaches a specified stop price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, limit_price, stop_price, exchange=None):
|
def __init__(self, limit_price, stop_price, exchange=None):
|
||||||
"""
|
"""
|
||||||
Store the given prices
|
Store the given prices
|
||||||
@@ -144,31 +142,20 @@ class StopLimitOrder(ExecutionStyle):
|
|||||||
def asymmetric_round_price_to_penny(price, prefer_round_down,
|
def asymmetric_round_price_to_penny(price, prefer_round_down,
|
||||||
diff=(0.0095 - .005)):
|
diff=(0.0095 - .005)):
|
||||||
"""
|
"""
|
||||||
Asymmetric rounding function for adjusting prices to two places in a way
|
Modified the original function because we do not want to round
|
||||||
that "improves" the price. For limit prices, this means preferring to
|
prices on crypto exchange.
|
||||||
round down on buys and preferring to round up on sells. For stop prices,
|
|
||||||
it means the reverse.
|
|
||||||
|
|
||||||
If prefer_round_down == True:
|
Parameters
|
||||||
When .05 below to .95 above a penny, use that penny.
|
----------
|
||||||
If prefer_round_down == False:
|
price: float
|
||||||
When .95 below to .05 above a penny, use that penny.
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
In math-speak:
|
|
||||||
If prefer_round_down: [<X-1>.0095, X.0195) -> round to X.01.
|
|
||||||
If not prefer_round_down: (<X-1>.0005, X.0105] -> round to X.01.
|
|
||||||
"""
|
"""
|
||||||
# Subtracting an epsilon from diff to enforce the open-ness of the upper
|
# TODO: consider overriding outside of the original function
|
||||||
# bound on buys and the lower bound on sells. Using the actual system
|
return price
|
||||||
# epsilon doesn't quite get there, so use a slightly less epsilon-ey value.
|
|
||||||
epsilon = float_info.epsilon * 10
|
|
||||||
diff = diff - epsilon
|
|
||||||
|
|
||||||
# relies on rounding half away from zero, unlike numpy's bankers' rounding
|
|
||||||
rounded = round(price - (diff if prefer_round_down else -diff), 2)
|
|
||||||
if zp_math.tolerant_equals(rounded, 0.0):
|
|
||||||
return 0.0
|
|
||||||
return rounded
|
|
||||||
|
|
||||||
|
|
||||||
def check_stoplimit_prices(price, label):
|
def check_stoplimit_prices(price, label):
|
||||||
|
|||||||
@@ -88,7 +88,10 @@ from six import itervalues, iteritems
|
|||||||
|
|
||||||
import catalyst.protocol as zp
|
import catalyst.protocol as zp
|
||||||
|
|
||||||
log = logbook.Logger('Performance')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
||||||
|
|
||||||
TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE
|
TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ import logbook
|
|||||||
from catalyst.assets import Future, Asset
|
from catalyst.assets import Future, Asset
|
||||||
from catalyst.utils.input_validation import expect_types
|
from catalyst.utils.input_validation import expect_types
|
||||||
|
|
||||||
log = logbook.Logger('Performance')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class Position(object):
|
class Position(object):
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ from catalyst.assets import (
|
|||||||
)
|
)
|
||||||
from . position import positiondict
|
from . position import positiondict
|
||||||
|
|
||||||
log = logbook.Logger('Performance')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
PositionStats = namedtuple('PositionStats',
|
PositionStats = namedtuple('PositionStats',
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ from __future__ import division
|
|||||||
import logbook
|
import logbook
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pandas.tseries.tools import normalize_date
|
|
||||||
|
|
||||||
from catalyst.finance.performance.period import PerformancePeriod
|
from catalyst.finance.performance.period import PerformancePeriod
|
||||||
from catalyst.errors import NoFurtherDataError
|
from catalyst.errors import NoFurtherDataError
|
||||||
@@ -70,7 +69,9 @@ import catalyst.finance.risk as risk
|
|||||||
|
|
||||||
from . position_tracker import PositionTracker
|
from . position_tracker import PositionTracker
|
||||||
|
|
||||||
log = logbook.Logger('Performance')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class PerformanceTracker(object):
|
class PerformanceTracker(object):
|
||||||
@@ -342,7 +343,7 @@ class PerformanceTracker(object):
|
|||||||
"""
|
"""
|
||||||
self.position_tracker.sync_last_sale_prices(dt, False, data_portal)
|
self.position_tracker.sync_last_sale_prices(dt, False, data_portal)
|
||||||
self.update_performance()
|
self.update_performance()
|
||||||
todays_date = normalize_date(dt)
|
todays_date = dt.normalize()
|
||||||
account = self.get_account(False)
|
account = self.get_account(False)
|
||||||
|
|
||||||
bench_returns = self.all_benchmark_returns.loc[todays_date:dt]
|
bench_returns = self.all_benchmark_returns.loc[todays_date:dt]
|
||||||
|
|||||||
@@ -18,28 +18,28 @@ import logbook
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pandas.tseries.tools import normalize_date
|
|
||||||
|
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
|
||||||
from . risk import (
|
from .risk import (
|
||||||
check_entry,
|
check_entry,
|
||||||
choose_treasury
|
choose_treasury
|
||||||
)
|
)
|
||||||
|
|
||||||
from empyrical import (
|
from catalyst.patches.stats import (
|
||||||
alpha_beta_aligned,
|
alpha_beta_aligned,
|
||||||
annual_volatility,
|
annual_volatility,
|
||||||
cum_returns,
|
|
||||||
downside_risk,
|
downside_risk,
|
||||||
information_ratio,
|
information_ratio,
|
||||||
max_drawdown,
|
max_drawdown,
|
||||||
sharpe_ratio,
|
sharpe_ratio,
|
||||||
sortino_ratio,
|
sortino_ratio,
|
||||||
|
cum_returns,
|
||||||
)
|
)
|
||||||
|
import warnings
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
log = logbook.Logger('Risk Cumulative')
|
log = logbook.Logger('Risk Cumulative', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
choose_treasury = functools.partial(choose_treasury, lambda *args: '10year',
|
choose_treasury = functools.partial(choose_treasury, lambda *args: '10year',
|
||||||
compound=False)
|
compound=False)
|
||||||
@@ -79,7 +79,7 @@ class RiskMetricsCumulative(object):
|
|||||||
# on the first day.
|
# on the first day.
|
||||||
self.day_before_start = self.start_session - self.sessions.freq
|
self.day_before_start = self.start_session - self.sessions.freq
|
||||||
|
|
||||||
last_day = normalize_date(sim_params.end_session)
|
last_day = sim_params.end_session.normalize()
|
||||||
if last_day not in self.sessions:
|
if last_day not in self.sessions:
|
||||||
last_day = pd.tseries.index.DatetimeIndex(
|
last_day = pd.tseries.index.DatetimeIndex(
|
||||||
[last_day]
|
[last_day]
|
||||||
@@ -143,6 +143,8 @@ class RiskMetricsCumulative(object):
|
|||||||
self.num_trading_days = 0
|
self.num_trading_days = 0
|
||||||
|
|
||||||
def update(self, dt, algorithm_returns, benchmark_returns, leverage):
|
def update(self, dt, algorithm_returns, benchmark_returns, leverage):
|
||||||
|
warnings.filterwarnings('error')
|
||||||
|
|
||||||
# Keep track of latest dt for use in to_dict and other methods
|
# Keep track of latest dt for use in to_dict and other methods
|
||||||
# that report current state.
|
# that report current state.
|
||||||
self.latest_dt = dt
|
self.latest_dt = dt
|
||||||
@@ -158,9 +160,13 @@ class RiskMetricsCumulative(object):
|
|||||||
if len(self.algorithm_returns) == 1:
|
if len(self.algorithm_returns) == 1:
|
||||||
self.algorithm_returns = np.append(0.0, self.algorithm_returns)
|
self.algorithm_returns = np.append(0.0, self.algorithm_returns)
|
||||||
|
|
||||||
self.algorithm_cumulative_returns[dt_loc] = cum_returns(
|
try:
|
||||||
self.algorithm_returns
|
self.algorithm_cumulative_returns[dt_loc] = cum_returns(
|
||||||
)[-1]
|
self.algorithm_returns
|
||||||
|
)[-1]
|
||||||
|
except Exception as e:
|
||||||
|
log.debug('unable to calculate cum returns: {}'.format(e))
|
||||||
|
self.algorithm_cumulative_returns[dt_loc] = np.nan
|
||||||
|
|
||||||
algo_cumulative_returns_to_date = \
|
algo_cumulative_returns_to_date = \
|
||||||
self.algorithm_cumulative_returns[:dt_loc + 1]
|
self.algorithm_cumulative_returns[:dt_loc + 1]
|
||||||
@@ -189,9 +195,15 @@ class RiskMetricsCumulative(object):
|
|||||||
if len(self.benchmark_returns) == 1:
|
if len(self.benchmark_returns) == 1:
|
||||||
self.benchmark_returns = np.append(0.0, self.benchmark_returns)
|
self.benchmark_returns = np.append(0.0, self.benchmark_returns)
|
||||||
|
|
||||||
self.benchmark_cumulative_returns[dt_loc] = cum_returns(
|
try:
|
||||||
self.benchmark_returns
|
self.benchmark_cumulative_returns[dt_loc] = cum_returns(
|
||||||
)[-1]
|
self.benchmark_returns
|
||||||
|
)[-1]
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(
|
||||||
|
'unable to calculate benchmark cum returns: {}'.format(e)
|
||||||
|
)
|
||||||
|
self.benchmark_cumulative_returns[dt_loc] = np.nan
|
||||||
|
|
||||||
benchmark_cumulative_returns_to_date = \
|
benchmark_cumulative_returns_to_date = \
|
||||||
self.benchmark_cumulative_returns[:dt_loc + 1]
|
self.benchmark_cumulative_returns[:dt_loc + 1]
|
||||||
@@ -263,24 +275,49 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
|
|||||||
self.sharpe[dt_loc] = sharpe_ratio(
|
self.sharpe[dt_loc] = sharpe_ratio(
|
||||||
self.algorithm_returns,
|
self.algorithm_returns,
|
||||||
)
|
)
|
||||||
self.downside_risk[dt_loc] = downside_risk(
|
|
||||||
self.algorithm_returns
|
try:
|
||||||
)
|
self.downside_risk[dt_loc] = downside_risk(
|
||||||
self.sortino[dt_loc] = sortino_ratio(
|
self.algorithm_returns
|
||||||
self.algorithm_returns,
|
)
|
||||||
_downside_risk=self.downside_risk[dt_loc]
|
except Exception as e:
|
||||||
)
|
log.debug(
|
||||||
|
'unable to calculate downside risk returns: {}'.format(e)
|
||||||
|
)
|
||||||
|
self.downside_risk[dt_loc] = np.nan
|
||||||
|
|
||||||
|
try:
|
||||||
|
risk = self.downside_risk[dt_loc]
|
||||||
|
self.sortino[dt_loc] = sortino_ratio(
|
||||||
|
self.algorithm_returns,
|
||||||
|
_downside_risk=risk
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(
|
||||||
|
'unable to calculate benchmark cum returns: {}'.format(e)
|
||||||
|
)
|
||||||
|
self.sortino[dt_loc] = np.nan
|
||||||
|
|
||||||
self.information[dt_loc] = information_ratio(
|
self.information[dt_loc] = information_ratio(
|
||||||
self.algorithm_returns,
|
self.algorithm_returns,
|
||||||
self.benchmark_returns,
|
self.benchmark_returns,
|
||||||
)
|
)
|
||||||
self.max_drawdown = max_drawdown(
|
try:
|
||||||
self.algorithm_returns
|
self.max_drawdown = max_drawdown(
|
||||||
)
|
self.algorithm_returns
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.debug(
|
||||||
|
'unable to calculate max drawdown: {}'.format(e)
|
||||||
|
)
|
||||||
|
self.max_drawdown = np.nan
|
||||||
|
|
||||||
self.max_drawdowns[dt_loc] = self.max_drawdown
|
self.max_drawdowns[dt_loc] = self.max_drawdown
|
||||||
self.max_leverage = self.calculate_max_leverage()
|
self.max_leverage = self.calculate_max_leverage()
|
||||||
self.max_leverages[dt_loc] = self.max_leverage
|
self.max_leverages[dt_loc] = self.max_leverage
|
||||||
|
|
||||||
|
warnings.resetwarnings()
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
Creates a dictionary representing the state of the risk report.
|
Creates a dictionary representing the state of the risk report.
|
||||||
@@ -292,18 +329,18 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
|
|||||||
rval = {
|
rval = {
|
||||||
'trading_days': self.num_trading_days,
|
'trading_days': self.num_trading_days,
|
||||||
'benchmark_volatility':
|
'benchmark_volatility':
|
||||||
self.benchmark_volatility[dt_loc],
|
self.benchmark_volatility[dt_loc],
|
||||||
'algo_volatility':
|
'algo_volatility':
|
||||||
self.algorithm_volatility[dt_loc],
|
self.algorithm_volatility[dt_loc],
|
||||||
'treasury_period_return': self.treasury_period_return,
|
'treasury_period_return': self.treasury_period_return,
|
||||||
# Though the two following keys say period return,
|
# Though the two following keys say period return,
|
||||||
# they would be more accurately called the cumulative return.
|
# they would be more accurately called the cumulative return.
|
||||||
# However, the keys need to stay the same, for now, for backwards
|
# However, the keys need to stay the same, for now, for backwards
|
||||||
# compatibility with existing consumers.
|
# compatibility with existing consumers.
|
||||||
'algorithm_period_return':
|
'algorithm_period_return':
|
||||||
self.algorithm_cumulative_returns[dt_loc],
|
self.algorithm_cumulative_returns[dt_loc],
|
||||||
'benchmark_period_return':
|
'benchmark_period_return':
|
||||||
self.benchmark_cumulative_returns[dt_loc],
|
self.benchmark_cumulative_returns[dt_loc],
|
||||||
'beta': self.beta[dt_loc],
|
'beta': self.beta[dt_loc],
|
||||||
'alpha': self.alpha[dt_loc],
|
'alpha': self.alpha[dt_loc],
|
||||||
'sharpe': self.sharpe[dt_loc],
|
'sharpe': self.sharpe[dt_loc],
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import warnings
|
||||||
|
|
||||||
import logbook
|
import logbook
|
||||||
|
|
||||||
@@ -23,20 +24,24 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from . import risk
|
from . import risk
|
||||||
from . risk import check_entry
|
from .risk import check_entry
|
||||||
|
|
||||||
from empyrical import (
|
from empyrical import (
|
||||||
alpha_beta_aligned,
|
alpha_beta_aligned,
|
||||||
annual_volatility,
|
annual_volatility,
|
||||||
cum_returns,
|
|
||||||
downside_risk,
|
downside_risk,
|
||||||
information_ratio,
|
information_ratio,
|
||||||
max_drawdown,
|
|
||||||
sharpe_ratio,
|
sharpe_ratio,
|
||||||
sortino_ratio
|
sortino_ratio
|
||||||
)
|
)
|
||||||
|
from catalyst.patches.stats import (
|
||||||
|
max_drawdown,
|
||||||
|
cum_returns,
|
||||||
|
)
|
||||||
|
|
||||||
log = logbook.Logger('Risk Period')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Risk Period', level=LOG_LEVEL)
|
||||||
|
|
||||||
choose_treasury = functools.partial(risk.choose_treasury,
|
choose_treasury = functools.partial(risk.choose_treasury,
|
||||||
risk.select_treasury_duration)
|
risk.select_treasury_duration)
|
||||||
@@ -76,14 +81,20 @@ class RiskMetricsPeriod(object):
|
|||||||
self.calculate_metrics()
|
self.calculate_metrics()
|
||||||
|
|
||||||
def calculate_metrics(self):
|
def calculate_metrics(self):
|
||||||
self.benchmark_period_returns = \
|
warnings.filterwarnings('error')
|
||||||
cum_returns(self.benchmark_returns).iloc[-1]
|
|
||||||
|
try:
|
||||||
|
self.benchmark_period_returns = \
|
||||||
|
cum_returns(self.benchmark_returns).iloc[-1]
|
||||||
|
except Exception:
|
||||||
|
# TODO: why is there an error
|
||||||
|
self.benchmark_period_returns = 0
|
||||||
|
|
||||||
self.algorithm_period_returns = \
|
self.algorithm_period_returns = \
|
||||||
cum_returns(self.algorithm_returns).iloc[-1]
|
cum_returns(self.algorithm_returns).iloc[-1]
|
||||||
|
|
||||||
if not self.algorithm_returns.index.equals(
|
if not self.algorithm_returns.index.equals(
|
||||||
self.benchmark_returns.index
|
self.benchmark_returns.index
|
||||||
):
|
):
|
||||||
message = "Mismatch between benchmark_returns ({bm_count}) and \
|
message = "Mismatch between benchmark_returns ({bm_count}) and \
|
||||||
algorithm_returns ({algo_count}) in range {start} : {end}"
|
algorithm_returns ({algo_count}) in range {start} : {end}"
|
||||||
@@ -126,10 +137,17 @@ class RiskMetricsPeriod(object):
|
|||||||
self.downside_risk = downside_risk(
|
self.downside_risk = downside_risk(
|
||||||
self.algorithm_returns.values
|
self.algorithm_returns.values
|
||||||
)
|
)
|
||||||
self.sortino = sortino_ratio(
|
|
||||||
self.algorithm_returns.values,
|
try:
|
||||||
_downside_risk=self.downside_risk,
|
risk = self.downside_risk
|
||||||
)
|
self.sortino = sortino_ratio(
|
||||||
|
self.algorithm_returns.values,
|
||||||
|
_downside_risk=risk,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# TODO: what causes it to error out?
|
||||||
|
self.sortino = 0
|
||||||
|
|
||||||
self.information = information_ratio(
|
self.information = information_ratio(
|
||||||
self.algorithm_returns.values,
|
self.algorithm_returns.values,
|
||||||
self.benchmark_returns.values,
|
self.benchmark_returns.values,
|
||||||
@@ -138,11 +156,13 @@ class RiskMetricsPeriod(object):
|
|||||||
self.algorithm_returns.values,
|
self.algorithm_returns.values,
|
||||||
self.benchmark_returns.values,
|
self.benchmark_returns.values,
|
||||||
)
|
)
|
||||||
self.excess_return = self.algorithm_period_returns - \
|
self.excess_return = self.algorithm_period_returns \
|
||||||
self.treasury_period_return
|
- self.treasury_period_return
|
||||||
self.max_drawdown = max_drawdown(self.algorithm_returns.values)
|
self.max_drawdown = max_drawdown(self.algorithm_returns.values)
|
||||||
self.max_leverage = self.calculate_max_leverage()
|
self.max_leverage = self.calculate_max_leverage()
|
||||||
|
|
||||||
|
warnings.resetwarnings()
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
Creates a dictionary representing the state of the risk report.
|
Creates a dictionary representing the state of the risk report.
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ from dateutil.relativedelta import relativedelta
|
|||||||
|
|
||||||
from . period import RiskMetricsPeriod
|
from . period import RiskMetricsPeriod
|
||||||
|
|
||||||
log = logbook.Logger('Risk Report')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Risk Report', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class RiskReport(object):
|
class RiskReport(object):
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ Risk Report
|
|||||||
import logbook
|
import logbook
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
log = logbook.Logger('Risk')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Risk', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
TREASURY_DURATIONS = [
|
TREASURY_DURATIONS = [
|
||||||
@@ -158,7 +160,8 @@ def choose_treasury(select_treasury, treasury_curves, start_session,
|
|||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
if search_day and trading_calendar.name != 'OPEN': # Supress warning for 'OPEN' calendar
|
# Supress warning for 'OPEN' calendar
|
||||||
|
if search_day and trading_calendar.name != 'OPEN':
|
||||||
if (search_dist is None or search_dist > 1) and \
|
if (search_dist is None or search_dist > 1) and \
|
||||||
search_days[0] <= end_session <= search_days[-1]:
|
search_days[0] <= end_session <= search_days[-1]:
|
||||||
message = "No rate within 1 trading day of end date = \
|
message = "No rate within 1 trading day of end date = \
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ DEFAULT_EQUITY_VOLUME_SLIPPAGE_BAR_LIMIT = 0.025
|
|||||||
DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT = 0.05
|
DEFAULT_FUTURE_VOLUME_SLIPPAGE_BAR_LIMIT = 0.05
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LiquidityExceeded(Exception):
|
class LiquidityExceeded(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ from functools import partial
|
|||||||
|
|
||||||
import logbook
|
import logbook
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from pandas.tslib import normalize_date
|
|
||||||
from six import string_types
|
from six import string_types
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
@@ -26,7 +25,9 @@ from catalyst.data.loader import load_market_data
|
|||||||
from catalyst.utils.calendars import get_calendar
|
from catalyst.utils.calendars import get_calendar
|
||||||
from catalyst.utils.memoize import remember_last
|
from catalyst.utils.memoize import remember_last
|
||||||
|
|
||||||
log = logbook.Logger('Trading')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Trading', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CAPITAL_BASE = 1e5
|
DEFAULT_CAPITAL_BASE = 1e5
|
||||||
@@ -93,11 +94,24 @@ class TradingEnvironment(object):
|
|||||||
if not trading_calendar:
|
if not trading_calendar:
|
||||||
trading_calendar = get_calendar("NYSE")
|
trading_calendar = get_calendar("NYSE")
|
||||||
|
|
||||||
self.benchmark_returns, self.treasury_curves = load(
|
# todo: uncomment and add a well defined benchmark
|
||||||
trading_calendar.day,
|
# self.benchmark_returns, self.treasury_curves = load(
|
||||||
trading_calendar.schedule.index,
|
# trading_calendar.day,
|
||||||
self.bm_symbol,
|
# trading_calendar.schedule.index,
|
||||||
)
|
# self.bm_symbol,
|
||||||
|
# exchange=exchange,
|
||||||
|
# )
|
||||||
|
|
||||||
|
start_data = get_calendar('OPEN').first_trading_session
|
||||||
|
end_data = pd.Timestamp.utcnow()
|
||||||
|
treasure_cols = ['1month', '3month', '6month', '1year', '2year',
|
||||||
|
'3year', '5year', '7year', '10year', '20year', '30year']
|
||||||
|
self.benchmark_returns = pd.DataFrame(data=0.001,
|
||||||
|
index=pd.date_range(start_data, end_data),
|
||||||
|
columns=['close'])
|
||||||
|
self.treasury_curves = pd.DataFrame(data=0.001,
|
||||||
|
index=pd.date_range(start_data, end_data),
|
||||||
|
columns=treasure_cols)
|
||||||
|
|
||||||
self.exchange_tz = exchange_tz
|
self.exchange_tz = exchange_tz
|
||||||
|
|
||||||
@@ -149,8 +163,8 @@ class SimulationParameters(object):
|
|||||||
# chop off any minutes or hours on the given start and end dates,
|
# chop off any minutes or hours on the given start and end dates,
|
||||||
# as we only support session labels here (and we represent session
|
# as we only support session labels here (and we represent session
|
||||||
# labels as midnight UTC).
|
# labels as midnight UTC).
|
||||||
self._start_session = normalize_date(start_session)
|
self._start_session = start_session.normalize()
|
||||||
self._end_session = normalize_date(end_session)
|
self._end_session = end_session.normalize()
|
||||||
self._capital_base = capital_base
|
self._capital_base = capital_base
|
||||||
|
|
||||||
self._emission_rate = emission_rate
|
self._emission_rate = emission_rate
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
from contextlib2 import ExitStack
|
from contextlib2 import ExitStack
|
||||||
from logbook import Logger, Processor
|
from logbook import Logger, Processor
|
||||||
from pandas.tslib import normalize_date
|
|
||||||
from catalyst.protocol import BarData
|
from catalyst.protocol import BarData
|
||||||
from catalyst.utils.api_support import ZiplineAPI
|
from catalyst.utils.api_support import ZiplineAPI
|
||||||
from six import viewkeys
|
from six import viewkeys
|
||||||
@@ -27,7 +26,9 @@ from catalyst.gens.sim_engine import (
|
|||||||
BEFORE_TRADING_START_BAR
|
BEFORE_TRADING_START_BAR
|
||||||
)
|
)
|
||||||
|
|
||||||
log = Logger('Trade Simulation')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('Trade Simulation', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class AlgorithmSimulator(object):
|
class AlgorithmSimulator(object):
|
||||||
@@ -227,7 +228,7 @@ class AlgorithmSimulator(object):
|
|||||||
elif action == SESSION_END:
|
elif action == SESSION_END:
|
||||||
# End of the session.
|
# End of the session.
|
||||||
if emission_rate == 'daily':
|
if emission_rate == 'daily':
|
||||||
handle_benchmark(normalize_date(dt))
|
handle_benchmark(dt).normalize()
|
||||||
execute_order_cancellation_policy()
|
execute_order_cancellation_policy()
|
||||||
|
|
||||||
yield self._get_daily_message(dt, algo, algo.perf_tracker)
|
yield self._get_daily_message(dt, algo, algo.perf_tracker)
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "name",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "approve",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "totalSupply",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "transferFrom",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "INITIAL_SUPPLY",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_subtractedValue",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "decreaseApproval",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "success",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "getAfterApproveTest",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "balanceOf",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "balance",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "symbol",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "transfer",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_addedValue",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "increaseApproval",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "success",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "allowance",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "testValue",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "constructor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "owner",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"name": "value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Approval",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"name": "value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Transfer",
|
||||||
|
"type": "event"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0xf0ee6b27b759c9893ce4f094b49ad28fd15a23e4
|
||||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user