mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-22 12:40:30 +08:00
Compare commits
938
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1e779b85e | ||
|
|
9bd966d627 | ||
|
|
eda8c8ce11 | ||
|
|
816b936737 | ||
|
|
c0455eca7c | ||
|
|
8527866b48 | ||
|
|
75385d034b | ||
|
|
580c33c957 | ||
|
|
c902949eb2 | ||
|
|
a8763a589e | ||
|
|
05f1b0ae5e | ||
|
|
a385732113 | ||
|
|
946954b308 | ||
|
|
8b81e66d57 | ||
|
|
a5ca8d0f15 | ||
|
|
069639a325 | ||
|
|
346780c3f0 | ||
|
|
886b2debe9 | ||
|
|
4d6c0ca699 | ||
|
|
7fe2e5c929 | ||
|
|
d3b06a083c | ||
|
|
ef40339058 | ||
|
|
1a98ea1639 | ||
|
|
9f96d5fbe4 | ||
|
|
94bb99da3b | ||
|
|
f66ef99202 | ||
|
|
2c9c4b0d05 | ||
|
|
1045dcd5bd | ||
|
|
e497fe76d7 | ||
|
|
1fe8ee6577 | ||
|
|
14047fde15 | ||
|
|
30e15a7b8b | ||
|
|
37a07e03e5 | ||
|
|
51cda7b2d4 | ||
|
|
6f591305e3 | ||
|
|
d48373a75a | ||
|
|
f7db6471ca | ||
|
|
2c122f6356 | ||
|
|
3949364d31 | ||
|
|
55f898562c | ||
|
|
fade9758a8 | ||
|
|
270175b6c4 | ||
|
|
4f1927bc34 | ||
|
|
05ae879c4f | ||
|
|
fcff8c7438 | ||
|
|
383370f9d8 | ||
|
|
0b16cce71e | ||
|
|
43e669d838 | ||
|
|
92139dd500 | ||
|
|
e22b2df45f | ||
|
|
00f2063e21 | ||
|
|
1f272f9ff8 | ||
|
|
10169734d2 | ||
|
|
141e414466 | ||
|
|
3df274431e | ||
|
|
e28459b19f | ||
|
|
7cf06909ce | ||
|
|
de8777d1c8 | ||
|
|
24141964bf | ||
|
|
cabea94973 | ||
|
|
e8e2346c60 | ||
|
|
6e47f704b4 | ||
|
|
dd97526489 | ||
|
|
6b4209e038 | ||
|
|
6f79f8b9b8 | ||
|
|
22ad375a52 | ||
|
|
07fb239d11 | ||
|
|
c4e53a7ceb | ||
|
|
c4bbbb4732 | ||
|
|
7576979003 | ||
|
|
ac70fb97d2 | ||
|
|
52f68fbea6 | ||
|
|
cbaccda564 | ||
|
|
0a0c96ba0c | ||
|
|
671f6febfe | ||
|
|
b4e0c6d2bb | ||
|
|
e3b7601f37 | ||
|
|
d2d297d348 | ||
|
|
8134b96ac3 | ||
|
|
4a62f5f366 | ||
|
|
0989919044 | ||
|
|
21dcbccbe3 | ||
|
|
cd8af67068 | ||
|
|
fb2afaff36 | ||
|
|
5ab98dbfd9 | ||
|
|
b55458f9c5 | ||
|
|
06e672a75e | ||
|
|
84afcf68ef | ||
|
|
4667f31b20 | ||
|
|
18228dcd8e | ||
|
|
bb17c171e5 | ||
|
|
0982998288 | ||
|
|
0f96bbb182 | ||
|
|
de839654fd | ||
|
|
daeef1759e | ||
|
|
05462dd6e2 | ||
|
|
bb9bf3bc9f | ||
|
|
53d1c7d7c3 | ||
|
|
d509dfb9a1 | ||
|
|
cd3d996892 | ||
|
|
3b6a293019 | ||
|
|
a0da21e6ea | ||
|
|
2b8b14bb9b | ||
|
|
2770648acb | ||
|
|
0ff32b38d7 | ||
|
|
a38a35d664 | ||
|
|
f6ceeb8be1 | ||
|
|
753004647b | ||
|
|
0fa6c37b68 | ||
|
|
78c2404e55 | ||
|
|
c8c626e15a | ||
|
|
2590afc6e8 | ||
|
|
885698d50b | ||
|
|
ec2ec49914 | ||
|
|
609ae752d0 | ||
|
|
8a042937d9 | ||
|
|
cbe52cea88 | ||
|
|
e49f4c6149 | ||
|
|
011915d4fc | ||
|
|
b5e01d99ef | ||
|
|
49a7b78b2e | ||
|
|
ad10349992 | ||
|
|
a579485825 | ||
|
|
e47a21318c | ||
|
|
cfe4df8f2b | ||
|
|
d0544a6581 | ||
|
|
e995e6f2ed | ||
|
|
94b1d5a40e | ||
|
|
8b17b7d4b3 | ||
|
|
ba590266c9 | ||
|
|
731e3a8678 | ||
|
|
440ff4205b | ||
|
|
73d66216dc | ||
|
|
1051ef1eb8 | ||
|
|
cc867dbdfe | ||
|
|
090ce3b256 | ||
|
|
3ba34aa755 | ||
|
|
11c0782e9a | ||
|
|
e55a6078d6 | ||
|
|
a40e5d932c | ||
|
|
c0c67340fe | ||
|
|
e6fe3a0e7d | ||
|
|
ad3c72a8fb | ||
|
|
ff1c673a9d | ||
|
|
2d56d253fa | ||
|
|
69b632db37 | ||
|
|
c59518bbeb | ||
|
|
a2fbb9ee7f | ||
|
|
26175180dc | ||
|
|
d9d41e3bb7 | ||
|
|
5c6fe19e8e | ||
|
|
1ab0cd7113 | ||
|
|
0a672c7343 | ||
|
|
912165da06 | ||
|
|
77f1013d49 | ||
|
|
0d3ba78177 | ||
|
|
b0b60391f0 | ||
|
|
fa76c29334 | ||
|
|
167d76afd3 | ||
|
|
c9351d4c18 | ||
|
|
595e30db8e | ||
|
|
8579ce7b1a | ||
|
|
c9bbc23bc5 | ||
|
|
e5ef118f3b | ||
|
|
8d5a2dd0ce | ||
|
|
8905051736 | ||
|
|
7412aa54f5 | ||
|
|
268b1cb6ad | ||
|
|
35143c3e1b | ||
|
|
d6034682f1 | ||
|
|
dcae4bf8a3 | ||
|
|
a72d7a7b34 | ||
|
|
a3edae9845 | ||
|
|
9f128cbde1 | ||
|
|
ec396bd1ea | ||
|
|
0f6dbcef3c | ||
|
|
c5b3ceecc1 | ||
|
|
3b3854c3bf | ||
|
|
8cc97d75b5 | ||
|
|
dde4974705 | ||
|
|
616f6e5e5d | ||
|
|
c70244fe7f | ||
|
|
5678d2d37b | ||
|
|
705044830b | ||
|
|
d39f012198 | ||
|
|
86fa28f9e8 | ||
|
|
281a17c592 | ||
|
|
5327fc9a05 | ||
|
|
b246f6a03b | ||
|
|
072466e08c | ||
|
|
a416662a9c | ||
|
|
43e0545856 | ||
|
|
bffb4bfea5 | ||
|
|
78ae3474ad | ||
|
|
da63a117e0 | ||
|
|
18ee06f0d4 | ||
|
|
d733605327 | ||
|
|
035311213e | ||
|
|
56a993bfc6 | ||
|
|
a1d055abb1 | ||
|
|
07561469bc | ||
|
|
560ff3cacf | ||
|
|
52667b4a90 | ||
|
|
f0e100bcf3 | ||
|
|
cb3926f81c | ||
|
|
9095b241f2 | ||
|
|
c0123b9caa | ||
|
|
3ff281079a | ||
|
|
06d8ddc3cc | ||
|
|
04cf61d03d | ||
|
|
a11574c942 | ||
|
|
dead9651b2 | ||
|
|
e3a50dee8f | ||
|
|
f7f17af2d8 | ||
|
|
b77d5ae6fb | ||
|
|
ec7cba2e31 | ||
|
|
0782e402d3 | ||
|
|
fa0594555c | ||
|
|
9effe84c8e | ||
|
|
a1d30c94ee | ||
|
|
362f2c2f7a | ||
|
|
278d2f6b2a | ||
|
|
7ba070f640 | ||
|
|
7196e1e498 | ||
|
|
5b8b2f68bc | ||
|
|
825866948b | ||
|
|
fe84ff3582 | ||
|
|
0b4b058065 | ||
|
|
b7b8c46d74 | ||
|
|
483012ccf6 | ||
|
|
450690801a | ||
|
|
8d275d8d83 | ||
|
|
21976dd651 | ||
|
|
e0060f61e8 | ||
|
|
1d1c244e84 | ||
|
|
2f33ddb023 | ||
|
|
cd91d518bb | ||
|
|
0ec8841ea0 | ||
|
|
acf345e1d3 | ||
|
|
26ffda9ca1 | ||
|
|
1d7d3fe33f | ||
|
|
12f1429a8c | ||
|
|
0c88ba21b9 | ||
|
|
9e0cf08c18 | ||
|
|
f9d55cf9cd | ||
|
|
bc27e369ff | ||
|
|
d5aa5b0c36 | ||
|
|
ec8880a066 | ||
|
|
9350759119 | ||
|
|
b35ec1b5cd | ||
|
|
845dea3e5b | ||
|
|
e6f2f562b3 | ||
|
|
569998179a | ||
|
|
f8eced41bf | ||
|
|
ad7f727c47 | ||
|
|
1212d2f0a1 | ||
|
|
276b65f527 | ||
|
|
18b0def1a1 | ||
|
|
e914b23c80 | ||
|
|
340a66afd5 | ||
|
|
d289e86895 | ||
|
|
c701f200c2 | ||
|
|
65381ecbcd | ||
|
|
63ad5a5b55 | ||
|
|
63951ad9e9 | ||
|
|
3dfc20ec1c | ||
|
|
dee19a0d8b | ||
|
|
a89d05dd56 | ||
|
|
0c49c5bbc7 | ||
|
|
84b24d2cee | ||
|
|
0182d62cc8 | ||
|
|
69a0e5c31b | ||
|
|
fc65ac33ca | ||
|
|
0bcdb640ca | ||
|
|
f0f8bac2ab | ||
|
|
0d0473cb44 | ||
|
|
066a7776d2 | ||
|
|
35203c6904 | ||
|
|
2792de1f92 | ||
|
|
ee19186e51 | ||
|
|
63c80109ec | ||
|
|
81b943c61d | ||
|
|
4916fb5fcc | ||
|
|
3e1f3c1e54 | ||
|
|
0e3421ead8 | ||
|
|
2010a7faf8 | ||
|
|
b81ddd7d73 | ||
|
|
02b26a97a5 | ||
|
|
24ce67a7cf | ||
|
|
7ef17ae3af | ||
|
|
5b27790126 | ||
|
|
c0355af316 | ||
|
|
8a25ac3af0 | ||
|
|
c0a76a5303 | ||
|
|
2860708a44 | ||
|
|
78e56c33ef | ||
|
|
825ba5717e | ||
|
|
f561e2ea54 | ||
|
|
83756d4660 | ||
|
|
58fb830ebd | ||
|
|
8292264229 | ||
|
|
c9254d395d | ||
|
|
fa56fe4954 | ||
|
|
3909269806 | ||
|
|
4071a52702 | ||
|
|
9a7e847ab7 | ||
|
|
b95088ca76 | ||
|
|
dcf1210043 | ||
|
|
40fc1c9185 | ||
|
|
beb4bd8c6e | ||
|
|
85eef5cb6b | ||
|
|
3004108464 | ||
|
|
d3b9da83c9 | ||
|
|
476e86a041 | ||
|
|
5d77a4bf94 | ||
|
|
2d1108fb7e | ||
|
|
6596c03351 | ||
|
|
de3702ab59 | ||
|
|
9252b62b2b | ||
|
|
799bad957d | ||
|
|
cb76f1fc16 | ||
|
|
cfdbe883b1 | ||
|
|
44563d351b | ||
|
|
fb10d9346b | ||
|
|
0d4ca54900 | ||
|
|
270e670551 | ||
|
|
a8d6ecd545 | ||
|
|
a4e445153c | ||
|
|
dd7fa4ba25 | ||
|
|
83ab7b3250 | ||
|
|
5b7124ac6f | ||
|
|
eef59f842c | ||
|
|
d176ba4d85 | ||
|
|
cf063abcf1 | ||
|
|
93c50899f8 | ||
|
|
159ed7321f | ||
|
|
ec447198c6 | ||
|
|
8e1f8d75f5 | ||
|
|
383d56b3bc | ||
|
|
63063aa709 | ||
|
|
5ab0880ad7 | ||
|
|
59acadbccf | ||
|
|
c2560c6079 | ||
|
|
e81287e8e1 | ||
|
|
54c8e6c358 | ||
|
|
cb6c5f09a6 | ||
|
|
8878d0ddd5 | ||
|
|
b4ed1fcff3 | ||
|
|
735d98fb6e | ||
|
|
cb41be012f | ||
|
|
db2440871e | ||
|
|
26558743ba | ||
|
|
7a0a214ac1 | ||
|
|
11a406c67a | ||
|
|
d378c9fca3 | ||
|
|
5fbd0251aa | ||
|
|
d9321f4795 | ||
|
|
38367b6794 | ||
|
|
6cd6163fd2 | ||
|
|
bb36ec1224 | ||
|
|
dda5334580 | ||
|
|
bd06ff14ed | ||
|
|
58850b86ad | ||
|
|
9786483ec0 | ||
|
|
9cdd696467 | ||
|
|
c5efba0c8a | ||
|
|
08b159d5da | ||
|
|
98a8fdfb09 | ||
|
|
f14e8e00ef | ||
|
|
96ba361e36 | ||
|
|
938bb70227 | ||
|
|
caee313012 | ||
|
|
d82bf7a1e3 | ||
|
|
e5f403deca | ||
|
|
beea8624d1 | ||
|
|
484f80dd01 | ||
|
|
1302d4152f | ||
|
|
8bf2da6ac0 | ||
|
|
2a9d8cebc3 | ||
|
|
0d1e5e2d1c | ||
|
|
44088dd4d5 | ||
|
|
b080d337f5 | ||
|
|
ccddc86d6b | ||
|
|
9ea1ee443a | ||
|
|
b90091e494 | ||
|
|
364f5cc199 | ||
|
|
83a221e986 | ||
|
|
f0b5b21c67 | ||
|
|
efcc87e089 | ||
|
|
006df906b0 | ||
|
|
1f56f20a7f | ||
|
|
25f826d5c7 | ||
|
|
ecbc7f890b | ||
|
|
fad98672e0 | ||
|
|
7e2838744c | ||
|
|
962c414ab7 | ||
|
|
df87dfb227 | ||
|
|
1d75143f54 | ||
|
|
2af63dfab7 | ||
|
|
cb50959efc | ||
|
|
a06503d8ec | ||
|
|
57b09ce1d7 | ||
|
|
64f77eb3e4 | ||
|
|
50c096c3e9 | ||
|
|
1e51dbec0a | ||
|
|
a1f09cffb8 | ||
|
|
b5ce101f5a | ||
|
|
aed2c64c99 | ||
|
|
dd560ba5f0 | ||
|
|
c1fe909dde | ||
|
|
2d5f5491ee | ||
|
|
a3dfa4a6fa | ||
|
|
037b2b0003 | ||
|
|
d023a8ed4f | ||
|
|
dfdf26e310 | ||
|
|
e4c0120876 | ||
|
|
9262381d17 | ||
|
|
225b6fe119 | ||
|
|
5a8443b5f1 | ||
|
|
e913519734 | ||
|
|
3095f8c573 | ||
|
|
4475a42b15 | ||
|
|
0aa4f129c1 | ||
|
|
fd74aad31c | ||
|
|
df6cb62925 | ||
|
|
d2cc337202 | ||
|
|
4b69a253ec | ||
|
|
56b5ac686a | ||
|
|
c6ad893c14 | ||
|
|
09318df534 | ||
|
|
7ab0b08e95 | ||
|
|
30bc01dbc0 | ||
|
|
555d460d26 | ||
|
|
0ee76d1e11 | ||
|
|
edab970973 | ||
|
|
83d0c45ec3 | ||
|
|
dea157741f | ||
|
|
12b6c273d1 | ||
|
|
d71feec46d | ||
|
|
47633b1601 | ||
|
|
3457b106fb | ||
|
|
8314586612 | ||
|
|
b501ecc736 | ||
|
|
de7c32b22c | ||
|
|
1888cf595f | ||
|
|
3cfbdeaca1 | ||
|
|
0fa4031f97 | ||
|
|
a19ec84a1d | ||
|
|
b120da4a4c | ||
|
|
584ff4d67a | ||
|
|
583f347bc3 | ||
|
|
c1063eac26 | ||
|
|
3e1a518ae6 | ||
|
|
1f71c8d068 | ||
|
|
3575351306 | ||
|
|
d217b275fb | ||
|
|
726edb0e32 | ||
|
|
c3004b2529 | ||
|
|
69b35a131a | ||
|
|
518f3d1f4b | ||
|
|
e5c941e592 | ||
|
|
9594d2779b | ||
|
|
399bf3085c | ||
|
|
9c05e5edfe | ||
|
|
48327266db | ||
|
|
5e8cc00625 | ||
|
|
9b8d6202f0 | ||
|
|
bd84338aca | ||
|
|
5842e981d7 | ||
|
|
adc9900860 | ||
|
|
96f8eeeb5f | ||
|
|
f1254ea79a | ||
|
|
40605e3c5d | ||
|
|
aa3f2220d9 | ||
|
|
f244a20e65 | ||
|
|
a60c9c683c | ||
|
|
b091b40604 | ||
|
|
ac8d21f1da | ||
|
|
d65af6c706 | ||
|
|
9aa6a585c4 | ||
|
|
66dd10a50c | ||
|
|
72203280d2 | ||
|
|
735aaf1b27 | ||
|
|
6734eb2afb | ||
|
|
bfc01546ae | ||
|
|
a7a350d930 | ||
|
|
613fdeab50 | ||
|
|
70b3055a68 | ||
|
|
5624e0f391 | ||
|
|
48324cf791 | ||
|
|
3546dbd64d | ||
|
|
be457b088c | ||
|
|
a61de3a4e7 | ||
|
|
b9b9b7d32b | ||
|
|
00ebae7729 | ||
|
|
08fb7333d6 | ||
|
|
907f30d013 | ||
|
|
0145cf8bf0 | ||
|
|
a4a571aff0 | ||
|
|
b4692c4121 | ||
|
|
48c360727b | ||
|
|
dad709590d | ||
|
|
57d35f6aac | ||
|
|
069058be3b | ||
|
|
604f8dd96f | ||
|
|
fc1f6aeae9 | ||
|
|
f7fdc56777 | ||
|
|
6ff1d55504 | ||
|
|
9e8212b25b | ||
|
|
f94a161c7a | ||
|
|
2b925ecb78 | ||
|
|
676fb9cb89 | ||
|
|
336867df15 | ||
|
|
a3df1e3cef | ||
|
|
a0e36d492d | ||
|
|
1a13e6172f | ||
|
|
2c087ef77b | ||
|
|
2b6fbdc202 | ||
|
|
24931bd4dd | ||
|
|
62bb389199 | ||
|
|
867b878c9a | ||
|
|
563a8b34f3 | ||
|
|
ad7ddd17ea | ||
|
|
66a214fae9 | ||
|
|
0f57dac4ab | ||
|
|
7f40f7a99d | ||
|
|
d5b901df3f | ||
|
|
9822ee5783 | ||
|
|
0eb461bd16 | ||
|
|
92516e6609 | ||
|
|
499680553f | ||
|
|
f9ce7f060e | ||
|
|
7fc2588571 | ||
|
|
4f9c6bd254 | ||
|
|
892b7cca00 | ||
|
|
9f10bbaf94 | ||
|
|
e89410dc30 | ||
|
|
8ccdb48c40 | ||
|
|
b714a23b44 | ||
|
|
7e505926b6 | ||
|
|
914dca1685 | ||
|
|
ac74a9dff5 | ||
|
|
e93087e452 | ||
|
|
8876092d29 | ||
|
|
c25da8f442 | ||
|
|
e07d5a202a | ||
|
|
e1bafe1ecc | ||
|
|
48c725b5ea | ||
|
|
0cbc2ca388 | ||
|
|
1e889987eb | ||
|
|
d18080553b | ||
|
|
16e3cb50cc | ||
|
|
0fbf7b428b | ||
|
|
a252bb1e3f | ||
|
|
57a0822b60 | ||
|
|
52b71af848 | ||
|
|
fc153999e2 | ||
|
|
a1f771c1fa | ||
|
|
eb2b10554b | ||
|
|
82cbac85c2 | ||
|
|
9294e39ea0 | ||
|
|
642e404982 | ||
|
|
473c8fddba | ||
|
|
7e3e00c772 | ||
|
|
a823cceabc | ||
|
|
426c2070c8 | ||
|
|
fa63fa6dd5 | ||
|
|
3f85140bf9 | ||
|
|
18096f750a | ||
|
|
546cb138ec | ||
|
|
202b557c48 | ||
|
|
0d10a20551 | ||
|
|
e82fef41dd | ||
|
|
eff2428068 | ||
|
|
086ea6be6b | ||
|
|
4ea60c2843 | ||
|
|
5e5339285f | ||
|
|
71d6aa7a15 | ||
|
|
ce37ea64a9 | ||
|
|
bf7fdd9685 | ||
|
|
9011d7b834 | ||
|
|
6efe8e7edc | ||
|
|
ea749b081f | ||
|
|
e4150ca966 | ||
|
|
3d7d2c139b | ||
|
|
71765dd3f4 | ||
|
|
7d8768e2e4 | ||
|
|
62f24dcad7 | ||
|
|
ce223db78c | ||
|
|
68ffd694e3 | ||
|
|
34d4e4b974 | ||
|
|
837d8824de | ||
|
|
2f16c08dcd | ||
|
|
3f8a86dbb5 | ||
|
|
95a56663d0 | ||
|
|
8f85bef9fe | ||
|
|
e1b8f34182 | ||
|
|
103993a8a8 | ||
|
|
cdfad2a54f | ||
|
|
047313b516 | ||
|
|
e52d831a94 | ||
|
|
81b7d5eadc | ||
|
|
dcf3124d26 | ||
|
|
7883f1fc06 | ||
|
|
c25b3d93f4 | ||
|
|
70bd8e5f64 | ||
|
|
c0380f9d9b | ||
|
|
ea5b2b030c | ||
|
|
4c40d25be4 | ||
|
|
7252a03d67 | ||
|
|
2a28f302ef | ||
|
|
58dca08d8b | ||
|
|
1d5f3572c9 | ||
|
|
fea7d899cd | ||
|
|
ee3c51f667 | ||
|
|
fcf3e50cde | ||
|
|
c9ebf9f45d | ||
|
|
2f097ead76 | ||
|
|
b6f8a67aad | ||
|
|
f3f138c450 | ||
|
|
e421a811e7 | ||
|
|
6eafdddb39 | ||
|
|
1bc55401b4 | ||
|
|
e304786fa0 | ||
|
|
f92b6afb31 | ||
|
|
5d9d9a97f5 | ||
|
|
1753a1ca3b | ||
|
|
4618303d36 | ||
|
|
50b65f1a77 | ||
|
|
7a08dd6bed | ||
|
|
25ba4369c3 | ||
|
|
148d2a5273 | ||
|
|
73f1ac6255 | ||
|
|
9f4f19524a | ||
|
|
2a92b892ed | ||
|
|
761ea7e417 | ||
|
|
117450a489 | ||
|
|
d79c43a46a | ||
|
|
8f150eb6ce | ||
|
|
99f6ecab3f | ||
|
|
a30f48e218 | ||
|
|
af85b22313 | ||
|
|
b504d29a78 | ||
|
|
0f47cf313d | ||
|
|
60b736b594 | ||
|
|
6cb06ffcdf | ||
|
|
9e6509daa3 | ||
|
|
5d7100542c | ||
|
|
25b38520fa | ||
|
|
cbd9bd068c | ||
|
|
8d8f057120 | ||
|
|
7d75615fd2 | ||
|
|
5e1a8224cd | ||
|
|
ebcc5f714a | ||
|
|
eb22f2ab6b | ||
|
|
af718b0f4a | ||
|
|
2fb7ec7612 | ||
|
|
9ce4077cc6 | ||
|
|
ef350f3889 | ||
|
|
f1c07708cd | ||
|
|
afc5297fe3 | ||
|
|
1c375806e7 | ||
|
|
de4d26cfd5 | ||
|
|
8ec76d4f38 | ||
|
|
3f078d4ef6 | ||
|
|
82273e296f | ||
|
|
db0eabe82a | ||
|
|
171736f2de | ||
|
|
bddb453272 | ||
|
|
f1919dc3af | ||
|
|
a5c7e93540 | ||
|
|
25517e2b15 | ||
|
|
96cc6b6588 | ||
|
|
5e20a1bdd1 | ||
|
|
9fd8ddda53 | ||
|
|
3477e26720 | ||
|
|
21a3de8ce5 | ||
|
|
9e0ef02ff6 | ||
|
|
b584ccc187 | ||
|
|
a9faf94945 | ||
|
|
9ea1af70a9 | ||
|
|
9cc2ecf8d5 | ||
|
|
c28ac1a4a5 | ||
|
|
c5936b86d6 | ||
|
|
ab9a5d7060 | ||
|
|
6e3bfe2d78 | ||
|
|
2522ca28ae | ||
|
|
467a4f8225 | ||
|
|
5370948196 | ||
|
|
ce76acce46 | ||
|
|
ec89402a3e | ||
|
|
918de6ad26 | ||
|
|
f77892240a | ||
|
|
ddef107fa1 | ||
|
|
46cf54b180 | ||
|
|
c6dc1db1f2 | ||
|
|
6aeba11176 | ||
|
|
ebbd79c7db | ||
|
|
9e517691bb | ||
|
|
48a44f321a | ||
|
|
4ba064dae7 | ||
|
|
f24db9c30c | ||
|
|
b639555404 | ||
|
|
493e18252d | ||
|
|
a9c02935c6 | ||
|
|
ed365dc5fe | ||
|
|
da8ed8919e | ||
|
|
52166e9958 | ||
|
|
4c3c2fd13c | ||
|
|
a1273cd669 | ||
|
|
af5f4be17c | ||
|
|
9bd6cab115 | ||
|
|
b9c2e8c1c6 | ||
|
|
9aa866e434 | ||
|
|
f30fc28b5c | ||
|
|
ef88dfdad2 | ||
|
|
74277490d5 | ||
|
|
f8734e8721 | ||
|
|
259f10a2d9 | ||
|
|
434d7c69d3 | ||
|
|
905df55a2d | ||
|
|
c4e114c28f | ||
|
|
dafe49fcca | ||
|
|
e43a3e8b1b | ||
|
|
e33c42b3a9 | ||
|
|
61a56492e6 | ||
|
|
fc3eac36aa | ||
|
|
b06ef66f44 | ||
|
|
0d8e99956e | ||
|
|
be30c0072d | ||
|
|
d2f0632101 | ||
|
|
29d5854aa1 | ||
|
|
80227eda57 | ||
|
|
ef04fe3a9b | ||
|
|
73a61caaeb | ||
|
|
657e5f92e7 | ||
|
|
8048cdcfb0 | ||
|
|
4a2faa3e13 | ||
|
|
72a8316ca7 | ||
|
|
6486e64530 | ||
|
|
7b74257f28 | ||
|
|
e21fa37e22 | ||
|
|
31cbdde7f0 | ||
|
|
0cf276d14a | ||
|
|
a7f345e651 | ||
|
|
f2a1263d90 | ||
|
|
ed08f25224 | ||
|
|
1d06a779b3 | ||
|
|
0fd35e7fd1 | ||
|
|
644a33e11b | ||
|
|
7ac024fb5c | ||
|
|
7c58bf1e5b | ||
|
|
cfd08f8d6b | ||
|
|
2856fd0ecf | ||
|
|
a0e1b881aa | ||
|
|
6aa885dbeb | ||
|
|
1f859d34e5 | ||
|
|
e6a647edf1 | ||
|
|
b729ddd54f | ||
|
|
6afdb318ee | ||
|
|
7a10d9392d | ||
|
|
9ad670bd46 | ||
|
|
3a82049a00 | ||
|
|
c66d3ad820 | ||
|
|
ff1bc51bd1 | ||
|
|
85ce093270 | ||
|
|
bb22af747d | ||
|
|
e6ef82fc4c | ||
|
|
6c7af70155 | ||
|
|
b1e50b3434 | ||
|
|
07cdd291a1 | ||
|
|
863e2ffbea | ||
|
|
bd420f938b | ||
|
|
1e030c77b2 | ||
|
|
b66d9ae976 | ||
|
|
631984e1f5 | ||
|
|
6a3db75a92 | ||
|
|
e7e4db0701 | ||
|
|
15ab8ac95e | ||
|
|
8056dd8a90 | ||
|
|
a8a2cc1582 | ||
|
|
43b9fa84d7 | ||
|
|
7f6db68fc6 | ||
|
|
eb6b6d046d | ||
|
|
548e0675be | ||
|
|
ddcba8733d | ||
|
|
b946c657b7 | ||
|
|
b0a93d57a0 | ||
|
|
948d813b84 | ||
|
|
d7eb845c7f | ||
|
|
28bca7cbd7 | ||
|
|
12101c55c8 | ||
|
|
8b2446aec6 | ||
|
|
1269250dde | ||
|
|
8f3e0cbfb2 | ||
|
|
ec7e4f2333 | ||
|
|
c5c7a4ac4a | ||
|
|
8595ca81cb | ||
|
|
baff6a84bc | ||
|
|
0d40b84550 | ||
|
|
3ab907ee4f | ||
|
|
e2300c73bf | ||
|
|
59d06a1883 | ||
|
|
95329180bd | ||
|
|
46c0c064fe | ||
|
|
af75a1f67c | ||
|
|
af20cdbe9d | ||
|
|
1bebad5b68 | ||
|
|
d1160439d2 | ||
|
|
fcde54297c | ||
|
|
f5d6bfca4d | ||
|
|
8d61e16df9 | ||
|
|
8863d007ce | ||
|
|
22de78fdb5 | ||
|
|
a813f245e2 | ||
|
|
a37141fb98 | ||
|
|
544dda115c | ||
|
|
3698362030 | ||
|
|
e574b7b042 | ||
|
|
624b4659f1 | ||
|
|
9919d47898 | ||
|
|
674a6200b4 | ||
|
|
bce159f275 | ||
|
|
5b333df10e | ||
|
|
c984cdb247 | ||
|
|
60c485c1ed | ||
|
|
7146edbe7d | ||
|
|
50e175bad2 | ||
|
|
5dfd2286b3 | ||
|
|
74ee33fd58 | ||
|
|
e22edbebae | ||
|
|
c713d4798f | ||
|
|
201e72aee1 | ||
|
|
e0cdc7bc3a | ||
|
|
339417d27f | ||
|
|
51da755c43 | ||
|
|
0cc08d79b4 | ||
|
|
3b502f53e4 | ||
|
|
a4131ea84b | ||
|
|
26797b132c | ||
|
|
bf49a6541f | ||
|
|
413ea3d9d5 | ||
|
|
0dd01650f1 | ||
|
|
0b9084edec | ||
|
|
fc8cf38a6e | ||
|
|
abdfe0265d | ||
|
|
cf6ec9fd73 | ||
|
|
419bd1e3b5 | ||
|
|
418e8901f4 | ||
|
|
bc87ea4efb | ||
|
|
6d473ce23f | ||
|
|
7ac0978f0c | ||
|
|
acb2cf118e | ||
|
|
a3c1f4ce36 | ||
|
|
20f9241252 | ||
|
|
1f829f8c92 | ||
|
|
a4638026c7 | ||
|
|
f0af856c13 | ||
|
|
902865cf71 | ||
|
|
78015066c8 | ||
|
|
6b2d614227 | ||
|
|
0c12e4a5a6 | ||
|
|
f247bc8d05 | ||
|
|
193b657bfe | ||
|
|
5be7e006e4 | ||
|
|
6fc5d7360d | ||
|
|
8af09141f9 | ||
|
|
4c2783f7ca | ||
|
|
ae121ec1dc | ||
|
|
c19d2855ad | ||
|
|
d52ff04626 | ||
|
|
2b5ae892f8 | ||
|
|
a3df81395a | ||
|
|
2eff193759 | ||
|
|
cb097a2b80 | ||
|
|
19c9bc2f41 | ||
|
|
09bd2abe2f | ||
|
|
aaa678d338 | ||
|
|
8e3cc484db | ||
|
|
ccde5cbf56 | ||
|
|
fce87015d2 | ||
|
|
5fb415eb12 | ||
|
|
653865b34c | ||
|
|
2030dd02a1 | ||
|
|
4865ade9b2 | ||
|
|
e3bd7e43be | ||
|
|
8834d0c35d | ||
|
|
53ca68e8fb | ||
|
|
a36766f1af | ||
|
|
04bf2f0b5e | ||
|
|
6c90a7c08b | ||
|
|
2a45bd1829 | ||
|
|
1d22fab127 | ||
|
|
c482a7cd59 | ||
|
|
1c0d407357 | ||
|
|
f14fcd9b07 | ||
|
|
bdc72ec4c0 | ||
|
|
9e0e2c26a5 | ||
|
|
1226286cab | ||
|
|
c5a3dae267 | ||
|
|
0f5784ab53 | ||
|
|
ecfe33b65b | ||
|
|
11f654a688 | ||
|
|
cbcb71bbad | ||
|
|
c53ef150ad | ||
|
|
a66731b9f3 | ||
|
|
5fe60c76a7 | ||
|
|
115f055c83 | ||
|
|
8fa51bdaab | ||
|
|
d917a64d45 | ||
|
|
a9b5b2ac2f | ||
|
|
5f686173f1 | ||
|
|
91276c7274 | ||
|
|
2d02431e6d | ||
|
|
0f4964fdfa | ||
|
|
fecfca9c59 | ||
|
|
10ab8dc875 | ||
|
|
1e7b566ed7 | ||
|
|
1444a78330 | ||
|
|
62d69db7f6 | ||
|
|
5107906413 | ||
|
|
765f9b6d57 | ||
|
|
4c59857e1f | ||
|
|
14a95449ca | ||
|
|
b6bacd2815 | ||
|
|
5723976e56 | ||
|
|
08a03edad7 | ||
|
|
9bd16aa313 | ||
|
|
ce717a1061 | ||
|
|
5a3588875a | ||
|
|
19429b9ad8 | ||
|
|
c7bd0d06e2 | ||
|
|
9685f3077a | ||
|
|
369eedf583 | ||
|
|
3136ef3f93 | ||
|
|
60f65cb8ce | ||
|
|
90a8ea2df3 | ||
|
|
34ec70abec |
+1
-3
@@ -1,3 +1 @@
|
|||||||
All the documentation for `Catalyst <https://github.com/enigmampc/catalyst>`_
|
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>`_.
|
||||||
can be found in the
|
|
||||||
`documentation website <https://enigmampc.github.io/catalyst>`_.
|
|
||||||
@@ -79,6 +79,7 @@ __all__ = [
|
|||||||
'gens',
|
'gens',
|
||||||
'run_algorithm',
|
'run_algorithm',
|
||||||
'utils',
|
'utils',
|
||||||
|
'exchange',
|
||||||
]
|
]
|
||||||
|
|
||||||
from ._version import get_versions
|
from ._version import get_versions
|
||||||
|
|||||||
+64
-342
@@ -8,9 +8,6 @@ import pandas as pd
|
|||||||
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_utils import delete_algo_folder
|
|
||||||
from catalyst.exchange.factory import get_exchange
|
|
||||||
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
|
||||||
|
|
||||||
@@ -30,19 +27,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 '
|
help='If --strict-extensions is passed then catalyst will not run if it'
|
||||||
'if it cannot load all of the specified extensions. If this is '
|
' cannot load all of the specified extensions. If this is not passed or'
|
||||||
'not passed or --non-strict-extensions is passed then the '
|
' --non-strict-extensions is passed then the failure will be logged but'
|
||||||
'failure will be logged but execution will continue.',
|
' 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 "
|
help="Don't load the default catalyst extension.py file in $ZIPLINE_HOME.",
|
||||||
"in $CATALYST_HOME.",
|
|
||||||
)
|
)
|
||||||
@click.version_option()
|
|
||||||
def main(extension, strict_extensions, default_extension):
|
def main(extension, strict_extensions, default_extension):
|
||||||
"""Top level catalyst entry point.
|
"""Top level catalyst entry point.
|
||||||
"""
|
"""
|
||||||
@@ -125,13 +120,13 @@ 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"
|
" the algotext. For example '-Dname=value'. The value may be any python"
|
||||||
" any python expression. These are evaluated in order so they"
|
" expression. These are evaluated in order so they may refer to previously"
|
||||||
" may refer to previously defined names.",
|
" defined names.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--data-frequency',
|
'--data-frequency',
|
||||||
type=click.Choice({'daily', 'minute'}),
|
type=click.Choice({'daily', '5-minute', 'minute'}),
|
||||||
default='daily',
|
default='daily',
|
||||||
show_default=True,
|
show_default=True,
|
||||||
help='The data frequency of the simulation.',
|
help='The data frequency of the simulation.',
|
||||||
@@ -139,6 +134,7 @@ 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.',
|
||||||
)
|
)
|
||||||
@@ -176,8 +172,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"
|
help="The location to write the perf data. If this is '-' the perf will"
|
||||||
" will be written to stdout.",
|
" be written to stdout.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--print-algo/--no-print-algo',
|
'--print-algo/--no-print-algo',
|
||||||
@@ -185,28 +181,39 @@ def ipython_only(option):
|
|||||||
default=False,
|
default=False,
|
||||||
help='Print the algorithm to stdout.',
|
help='Print the algorithm to stdout.',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'-s',
|
||||||
|
'--start',
|
||||||
|
type=Date(tz='utc', as_timestamp=True),
|
||||||
|
help='The start date of the simulation.',
|
||||||
|
)
|
||||||
@ipython_only(click.option(
|
@ipython_only(click.option(
|
||||||
'--local-namespace/--no-local-namespace',
|
'--local-namespace/--no-local-namespace',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=None,
|
default=None,
|
||||||
help='Should the algorithm methods be resolved in the local namespace.'
|
help='Should the algorithm methods be resolved in the local namespace.'
|
||||||
))
|
))
|
||||||
|
@click.option(
|
||||||
|
'--live/--no-live',
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help='Enable live trading.',
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
type=click.Choice({'bitfinex'}),
|
||||||
help='The name of the targeted exchange (supported: bitfinex,'
|
help='The name of the exchange (supported: bitfinex).',
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-n',
|
'-n',
|
||||||
'--algo-namespace',
|
'--algo-name',
|
||||||
help='A label assigned to the algorithm for data storage purposes.'
|
help='A label assigned to the algorithm for tracking purposes.',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-c',
|
'-c',
|
||||||
'--base-currency',
|
'--reference-currency',
|
||||||
help='The base currency used to calculate statistics '
|
help='The reference currency used to calculate statistics '
|
||||||
'(e.g. usd, btc, eth).',
|
'(e.g. usd, btc, eth).',
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@@ -223,44 +230,43 @@ def run(ctx,
|
|||||||
output,
|
output,
|
||||||
print_algo,
|
print_algo,
|
||||||
local_namespace,
|
local_namespace,
|
||||||
|
live,
|
||||||
exchange_name,
|
exchange_name,
|
||||||
algo_namespace,
|
algo_namespace,
|
||||||
base_currency):
|
base_currency):
|
||||||
"""Run a backtest for the given algorithm.
|
"""Run a backtest for the given algorithm.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if live:
|
||||||
|
if exchange_name is None:
|
||||||
|
ctx.fail("must specify an exchange name '-x' in live execution "
|
||||||
|
"mode '--live'")
|
||||||
|
if algo_namespace is None:
|
||||||
|
ctx.fail("must specify an algorithm name '-n' in live execution "
|
||||||
|
"mode '--live'")
|
||||||
|
if base_currency is None:
|
||||||
|
ctx.fail("must specify a reference currency '-c' in live "
|
||||||
|
"execution mode '--live'")
|
||||||
|
else:
|
||||||
|
# check that the start and end dates are passed correctly
|
||||||
|
if start is None and end is None:
|
||||||
|
# check both at the same time to avoid the case where a user
|
||||||
|
# does not pass either of these and then passes the first only
|
||||||
|
# to be told they need to pass the second argument also
|
||||||
|
ctx.fail(
|
||||||
|
"must specify dates with '-s' / '--start' and '-e' / '--end'",
|
||||||
|
)
|
||||||
|
if start is None:
|
||||||
|
ctx.fail("must specify a start date with '-s' / '--start'")
|
||||||
|
if end is None:
|
||||||
|
ctx.fail("must specify an end date with '-e' / '--end'")
|
||||||
|
|
||||||
if (algotext is not None) == (algofile is not None):
|
if (algotext is not None) == (algofile is not None):
|
||||||
ctx.fail(
|
ctx.fail(
|
||||||
"must specify exactly one of '-f' / '--algofile' or"
|
"must specify exactly one of '-f' / '--algofile' or"
|
||||||
" '-t' / '--algotext'",
|
" '-t' / '--algotext'",
|
||||||
)
|
)
|
||||||
|
|
||||||
# check that the start and end dates are passed correctly
|
|
||||||
if start is None and end is None:
|
|
||||||
# check both at the same time to avoid the case where a user
|
|
||||||
# does not pass either of these and then passes the first only
|
|
||||||
# to be told they need to pass the second argument also
|
|
||||||
ctx.fail(
|
|
||||||
"must specify dates with '-s' / '--start' and '-e' / '--end'"
|
|
||||||
" in backtest mode",
|
|
||||||
)
|
|
||||||
if start is None:
|
|
||||||
ctx.fail("must specify a start date with '-s' / '--start'"
|
|
||||||
" in backtest mode")
|
|
||||||
if end is None:
|
|
||||||
ctx.fail("must specify an end date with '-e' / '--end'"
|
|
||||||
" in backtest mode")
|
|
||||||
|
|
||||||
if exchange_name is None:
|
|
||||||
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'"
|
|
||||||
" in backtest mode")
|
|
||||||
|
|
||||||
perf = _run(
|
perf = _run(
|
||||||
initialize=None,
|
initialize=None,
|
||||||
handle_data=None,
|
handle_data=None,
|
||||||
@@ -280,11 +286,10 @@ def run(ctx,
|
|||||||
print_algo=print_algo,
|
print_algo=print_algo,
|
||||||
local_namespace=local_namespace,
|
local_namespace=local_namespace,
|
||||||
environ=os.environ,
|
environ=os.environ,
|
||||||
live=False,
|
live=live,
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
@@ -328,288 +333,15 @@ def catalyst_magic(line, cell=None):
|
|||||||
raise ValueError('main returned non-zero status code: %d' % e.code)
|
raise ValueError('main returned non-zero status code: %d' % e.code)
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
|
||||||
@click.option(
|
|
||||||
'-f',
|
|
||||||
'--algofile',
|
|
||||||
default=None,
|
|
||||||
type=click.File('r'),
|
|
||||||
help='The file that contains the algorithm to run.',
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'-t',
|
|
||||||
'--algotext',
|
|
||||||
help='The algorithm script to run.',
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'-D',
|
|
||||||
'--define',
|
|
||||||
multiple=True,
|
|
||||||
help="Define a name to be bound in the namespace before executing"
|
|
||||||
" the algotext. For example '-Dname=value'. The value may be"
|
|
||||||
" any python expression. These are evaluated in order so they"
|
|
||||||
" may refer to previously defined names.",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'-o',
|
|
||||||
'--output',
|
|
||||||
default='-',
|
|
||||||
metavar='FILENAME',
|
|
||||||
show_default=True,
|
|
||||||
help="The location to write the perf data. If this is '-' the perf will"
|
|
||||||
" be written to stdout.",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'--print-algo/--no-print-algo',
|
|
||||||
is_flag=True,
|
|
||||||
default=False,
|
|
||||||
help='Print the algorithm to stdout.',
|
|
||||||
)
|
|
||||||
@ipython_only(click.option(
|
|
||||||
'--local-namespace/--no-local-namespace',
|
|
||||||
is_flag=True,
|
|
||||||
default=None,
|
|
||||||
help='Should the algorithm methods be resolved in the local namespace.'
|
|
||||||
))
|
|
||||||
@click.option(
|
|
||||||
'-x',
|
|
||||||
'--exchange-name',
|
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
|
||||||
help='The name of the targeted exchange (supported: bitfinex,'
|
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'-n',
|
|
||||||
'--algo-namespace',
|
|
||||||
help='A label assigned to the algorithm for data storage purposes.'
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'-c',
|
|
||||||
'--base-currency',
|
|
||||||
help='The base currency used to calculate statistics '
|
|
||||||
'(e.g. usd, btc, eth).',
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'--live-graph/--no-live-graph',
|
|
||||||
is_flag=True,
|
|
||||||
default=False,
|
|
||||||
help='Display live graph.',
|
|
||||||
)
|
|
||||||
@click.pass_context
|
|
||||||
def live(ctx,
|
|
||||||
algofile,
|
|
||||||
algotext,
|
|
||||||
define,
|
|
||||||
output,
|
|
||||||
print_algo,
|
|
||||||
local_namespace,
|
|
||||||
exchange_name,
|
|
||||||
algo_namespace,
|
|
||||||
base_currency,
|
|
||||||
live_graph):
|
|
||||||
"""Trade live with the given algorithm.
|
|
||||||
"""
|
|
||||||
if (algotext is not None) == (algofile is not None):
|
|
||||||
ctx.fail(
|
|
||||||
"must specify exactly one of '-f' / '--algofile' or"
|
|
||||||
" '-t' / '--algotext'",
|
|
||||||
)
|
|
||||||
|
|
||||||
if exchange_name is None:
|
|
||||||
ctx.fail("must specify an exchange name '-x'")
|
|
||||||
if algo_namespace is None:
|
|
||||||
ctx.fail("must specify an algorithm name '-n' in live execution mode")
|
|
||||||
if base_currency is None:
|
|
||||||
ctx.fail("must specify a base currency '-c' in live execution mode")
|
|
||||||
|
|
||||||
perf = _run(
|
|
||||||
initialize=None,
|
|
||||||
handle_data=None,
|
|
||||||
before_trading_start=None,
|
|
||||||
analyze=None,
|
|
||||||
algofile=algofile,
|
|
||||||
algotext=algotext,
|
|
||||||
defines=define,
|
|
||||||
data_frequency=None,
|
|
||||||
capital_base=None,
|
|
||||||
data=None,
|
|
||||||
bundle=None,
|
|
||||||
bundle_timestamp=None,
|
|
||||||
start=None,
|
|
||||||
end=None,
|
|
||||||
output=output,
|
|
||||||
print_algo=print_algo,
|
|
||||||
local_namespace=local_namespace,
|
|
||||||
environ=os.environ,
|
|
||||||
live=True,
|
|
||||||
exchange=exchange_name,
|
|
||||||
algo_namespace=algo_namespace,
|
|
||||||
base_currency=base_currency,
|
|
||||||
live_graph=live_graph
|
|
||||||
)
|
|
||||||
|
|
||||||
if output == '-':
|
|
||||||
click.echo(str(perf))
|
|
||||||
elif output != os.devnull: # make the catalyst magic not write any data
|
|
||||||
perf.to_pickle(output)
|
|
||||||
|
|
||||||
return perf
|
|
||||||
|
|
||||||
|
|
||||||
@main.command(name='ingest-exchange')
|
|
||||||
@click.option(
|
|
||||||
'-x',
|
|
||||||
'--exchange-name',
|
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
|
||||||
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
|
||||||
@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.option(
|
|
||||||
'-i',
|
|
||||||
'--include-symbols',
|
|
||||||
default=None,
|
|
||||||
help='A list of symbols to ingest (optional comma separated list)',
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'--exclude-symbols',
|
|
||||||
default=None,
|
|
||||||
help='A list of symbols to exclude from the ingestion '
|
|
||||||
'(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(
|
|
||||||
'--show-progress/--no-show-progress',
|
|
||||||
default=True,
|
|
||||||
help='Print progress information to the terminal.'
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
'--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.'
|
|
||||||
)
|
|
||||||
def ingest_exchange(exchange_name, data_frequency, start, end,
|
|
||||||
include_symbols, exclude_symbols, csv, show_progress,
|
|
||||||
verbose, validate):
|
|
||||||
"""
|
|
||||||
Ingest data for the given exchange.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if exchange_name is None:
|
|
||||||
ctx.fail("must specify an exchange name '-x'")
|
|
||||||
|
|
||||||
exchange_bundle = ExchangeBundle(exchange_name)
|
|
||||||
|
|
||||||
click.echo('Ingesting exchange bundle {}...'.format(exchange_name))
|
|
||||||
exchange_bundle.ingest(
|
|
||||||
data_frequency=data_frequency,
|
|
||||||
include_symbols=include_symbols,
|
|
||||||
exclude_symbols=exclude_symbols,
|
|
||||||
start=start,
|
|
||||||
end=end,
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
delete_algo_folder(algo_namespace)
|
|
||||||
click.echo('Done')
|
|
||||||
|
|
||||||
|
|
||||||
@main.command(name='clean-exchange')
|
|
||||||
@click.option(
|
|
||||||
'-x',
|
|
||||||
'--exchange-name',
|
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
|
||||||
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
|
||||||
@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))
|
|
||||||
exchange_bundle.clean(
|
|
||||||
data_frequency=data_frequency,
|
|
||||||
)
|
|
||||||
click.echo('Done')
|
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
'-b',
|
'-b',
|
||||||
'--bundle',
|
'--bundle',
|
||||||
|
default='poloniex',
|
||||||
metavar='BUNDLE-NAME',
|
metavar='BUNDLE-NAME',
|
||||||
default=None,
|
show_default=True,
|
||||||
show_default=False,
|
|
||||||
help='The data bundle to ingest.',
|
help='The data bundle to ingest.',
|
||||||
)
|
)
|
||||||
@click.option(
|
|
||||||
'-x',
|
|
||||||
'--exchange-name',
|
|
||||||
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
|
||||||
help='The name of the exchange bundle to ingest (supported: bitfinex,'
|
|
||||||
' bittrex, poloniex).',
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
'-c',
|
'-c',
|
||||||
'--compile-locally',
|
'--compile-locally',
|
||||||
@@ -628,12 +360,9 @@ def clean_exchange(ctx, exchange_name, data_frequency):
|
|||||||
default=True,
|
default=True,
|
||||||
help='Print progress information to the terminal.'
|
help='Print progress information to the terminal.'
|
||||||
)
|
)
|
||||||
@click.pass_context
|
def ingest(bundle, compile_locally, assets_version, show_progress):
|
||||||
def ingest(ctx, bundle, exchange_name, compile_locally, assets_version,
|
|
||||||
show_progress):
|
|
||||||
"""Ingest the data for the given bundle.
|
"""Ingest the data for the given bundle.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
bundles_module.ingest(
|
bundles_module.ingest(
|
||||||
bundle,
|
bundle,
|
||||||
os.environ,
|
os.environ,
|
||||||
@@ -653,26 +382,19 @@ def ingest(ctx, bundle, exchange_name, compile_locally, assets_version,
|
|||||||
show_default=True,
|
show_default=True,
|
||||||
help='The data bundle to clean.',
|
help='The data bundle to clean.',
|
||||||
)
|
)
|
||||||
@click.option(
|
|
||||||
'-x',
|
|
||||||
'--exchange_name',
|
|
||||||
metavar='EXCHANGE-NAME',
|
|
||||||
show_default=True,
|
|
||||||
help='The exchange bundle name to clean.',
|
|
||||||
)
|
|
||||||
@click.option(
|
@click.option(
|
||||||
'-e',
|
'-e',
|
||||||
'--before',
|
'--before',
|
||||||
type=Timestamp(),
|
type=Timestamp(),
|
||||||
help='Clear all data before TIMESTAMP.'
|
help='Clear all data before TIMESTAMP.'
|
||||||
' This may not be passed with -k / --keep-last',
|
' This may not be passed with -k / --keep-last',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-a',
|
'-a',
|
||||||
'--after',
|
'--after',
|
||||||
type=Timestamp(),
|
type=Timestamp(),
|
||||||
help='Clear all data after TIMESTAMP'
|
help='Clear all data after TIMESTAMP'
|
||||||
' This may not be passed with -k / --keep-last',
|
' This may not be passed with -k / --keep-last',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-k',
|
'-k',
|
||||||
@@ -680,10 +402,10 @@ def ingest(ctx, bundle, exchange_name, compile_locally, assets_version,
|
|||||||
type=int,
|
type=int,
|
||||||
metavar='N',
|
metavar='N',
|
||||||
help='Clear all but the last N downloads.'
|
help='Clear all but the last N downloads.'
|
||||||
' 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 bundles from 'ingest'.
|
"""Clean up data downloaded with the ingest command.
|
||||||
"""
|
"""
|
||||||
bundles_module.clean(
|
bundles_module.clean(
|
||||||
bundle,
|
bundle,
|
||||||
|
|||||||
+51
-21
@@ -125,7 +125,6 @@ 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_if_near_integer,
|
||||||
round_nearest
|
|
||||||
)
|
)
|
||||||
from catalyst.utils.pandas_utils import clear_dataframe_indexer_caches
|
from catalyst.utils.pandas_utils import clear_dataframe_indexer_caches
|
||||||
from catalyst.utils.preprocess import preprocess
|
from catalyst.utils.preprocess import preprocess
|
||||||
@@ -134,13 +133,15 @@ from catalyst.utils.security_list import SecurityList
|
|||||||
import catalyst.protocol
|
import catalyst.protocol
|
||||||
from catalyst.sources.requests_csv import PandasRequestsCSV
|
from catalyst.sources.requests_csv import PandasRequestsCSV
|
||||||
|
|
||||||
from catalyst.gens.sim_engine import MinuteSimulationClock
|
from catalyst.gens.sim_engine import (
|
||||||
|
MinuteSimulationClock,
|
||||||
|
FiveMinuteSimulationClock,
|
||||||
|
)
|
||||||
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("CatalystLog", level=LOG_LEVEL)
|
log = logbook.Logger("ZiplineLog")
|
||||||
|
|
||||||
|
|
||||||
class TradingAlgorithm(object):
|
class TradingAlgorithm(object):
|
||||||
@@ -172,7 +173,7 @@ class TradingAlgorithm(object):
|
|||||||
algo_filename : str, optional
|
algo_filename : str, optional
|
||||||
The filename for the algoscript. This will be used in exception
|
The filename for the algoscript. This will be used in exception
|
||||||
tracebacks. default: '<string>'.
|
tracebacks. default: '<string>'.
|
||||||
data_frequency : {'daily', 'minute'}, optional
|
data_frequency : {'daily', '5-minute', 'minute'}, optional
|
||||||
The duration of the bars.
|
The duration of the bars.
|
||||||
instant_fill : bool, optional
|
instant_fill : bool, optional
|
||||||
Whether to fill orders immediately or on next bar. default: False
|
Whether to fill orders immediately or on next bar. default: False
|
||||||
@@ -225,7 +226,7 @@ class TradingAlgorithm(object):
|
|||||||
script : str
|
script : str
|
||||||
Algoscript that contains initialize and
|
Algoscript that contains initialize and
|
||||||
handle_data function definition.
|
handle_data function definition.
|
||||||
data_frequency : {'daily', 'minute'}
|
data_frequency : {'daily', '5-minute', 'minute'}
|
||||||
The duration of the bars.
|
The duration of the bars.
|
||||||
capital_base : float <default: 1.0e5>
|
capital_base : float <default: 1.0e5>
|
||||||
How much capital to start with.
|
How much capital to start with.
|
||||||
@@ -433,6 +434,8 @@ class TradingAlgorithm(object):
|
|||||||
if get_loader is not None:
|
if get_loader is not None:
|
||||||
if data_frequency == 'daily':
|
if data_frequency == 'daily':
|
||||||
all_dates = self.trading_calendar.all_sessions
|
all_dates = self.trading_calendar.all_sessions
|
||||||
|
elif data_frequency == '5-minute':
|
||||||
|
all_dates = self.trading_calendar.all_five_minutes
|
||||||
elif data_frequency == 'minute':
|
elif data_frequency == 'minute':
|
||||||
all_dates = self.trading_calendar.all_minutes
|
all_dates = self.trading_calendar.all_minutes
|
||||||
else:
|
else:
|
||||||
@@ -464,7 +467,7 @@ class TradingAlgorithm(object):
|
|||||||
self._in_before_trading_start = True
|
self._in_before_trading_start = True
|
||||||
|
|
||||||
with handle_non_market_minutes(data) if \
|
with handle_non_market_minutes(data) if \
|
||||||
self.data_frequency == 'minute' else ExitStack():
|
self.data_frequency in ('minute', '5-minute') else ExitStack():
|
||||||
self._before_trading_start(self, data)
|
self._before_trading_start(self, data)
|
||||||
|
|
||||||
self._in_before_trading_start = False
|
self._in_before_trading_start = False
|
||||||
@@ -520,10 +523,11 @@ class TradingAlgorithm(object):
|
|||||||
market_closes = trading_o_and_c['market_close']
|
market_closes = trading_o_and_c['market_close']
|
||||||
minutely_emission = False
|
minutely_emission = False
|
||||||
|
|
||||||
if self.sim_params.data_frequency == 'minute':
|
if self.sim_params.data_frequency in set(('minute', '5-minute')):
|
||||||
market_opens = trading_o_and_c['market_open']
|
market_opens = trading_o_and_c['market_open']
|
||||||
|
|
||||||
minutely_emission = self.sim_params.emission_rate == 'minute'
|
minutely_emission = self.sim_params.emission_rate in \
|
||||||
|
set(('minute', '5-minute'))
|
||||||
else:
|
else:
|
||||||
# in daily mode, we want to have one bar per session, timestamped
|
# in daily mode, we want to have one bar per session, timestamped
|
||||||
# as the last minute of the session.
|
# as the last minute of the session.
|
||||||
@@ -547,6 +551,15 @@ class TradingAlgorithm(object):
|
|||||||
'UTC',
|
'UTC',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.sim_params.data_frequency == '5-minute':
|
||||||
|
return FiveMinuteSimulationClock(
|
||||||
|
self.sim_params.sessions,
|
||||||
|
execution_opens,
|
||||||
|
execution_closes,
|
||||||
|
before_trading_start_minutes,
|
||||||
|
minute_emission=minutely_emission,
|
||||||
|
)
|
||||||
|
|
||||||
return MinuteSimulationClock(
|
return MinuteSimulationClock(
|
||||||
self.sim_params.sessions,
|
self.sim_params.sessions,
|
||||||
execution_opens,
|
execution_opens,
|
||||||
@@ -678,6 +691,8 @@ class TradingAlgorithm(object):
|
|||||||
time_count = times.nunique()
|
time_count = times.nunique()
|
||||||
if time_count == 1:
|
if time_count == 1:
|
||||||
self.sim_params.data_frequency = 'daily'
|
self.sim_params.data_frequency = 'daily'
|
||||||
|
elif time_count == 288:
|
||||||
|
self.sim_params.data_frequency = '5-minute'
|
||||||
else:
|
else:
|
||||||
self.sim_params.data_frequency = 'minute'
|
self.sim_params.data_frequency = 'minute'
|
||||||
|
|
||||||
@@ -699,6 +714,8 @@ class TradingAlgorithm(object):
|
|||||||
|
|
||||||
if self.sim_params.data_frequency == 'daily':
|
if self.sim_params.data_frequency == 'daily':
|
||||||
equity_reader_arg = 'equity_daily_reader'
|
equity_reader_arg = 'equity_daily_reader'
|
||||||
|
elif self.sim_params.data_frequency == '5-minute':
|
||||||
|
equity_daily_reader = 'equity_5_minute_reader'
|
||||||
elif self.sim_params.data_frequency == 'minute':
|
elif self.sim_params.data_frequency == 'minute':
|
||||||
equity_reader_arg = 'equity_minute_reader'
|
equity_reader_arg = 'equity_minute_reader'
|
||||||
equity_reader = PanelBarReader(
|
equity_reader = PanelBarReader(
|
||||||
@@ -942,9 +959,9 @@ class TradingAlgorithm(object):
|
|||||||
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', '5-minute', 'minute'}
|
||||||
data_frequency tells the algorithm if it is running with
|
data_frequency tells the algorithm if it is running with
|
||||||
daily or minute mode.
|
daily, minute, or five-minute mode.
|
||||||
start : datetime
|
start : datetime
|
||||||
The start date for the simulation.
|
The start date for the simulation.
|
||||||
end : datetime
|
end : datetime
|
||||||
@@ -1119,11 +1136,20 @@ class TradingAlgorithm(object):
|
|||||||
'time_rule= when calling schedule_function without '
|
'time_rule= when calling schedule_function without '
|
||||||
'specifying a date_rule', stacklevel=3)
|
'specifying a date_rule', stacklevel=3)
|
||||||
|
|
||||||
|
freq = self.sim_params.data_frequency
|
||||||
|
|
||||||
|
freq = self.sim_params.data_frequency
|
||||||
|
|
||||||
date_rule = date_rule or date_rules.every_day()
|
date_rule = date_rule or date_rules.every_day()
|
||||||
time_rule = ((time_rule or time_rules.every_minute())
|
if freq is 'daily':
|
||||||
if self.sim_params.data_frequency == 'minute' else
|
# ignore time rule in daily mode
|
||||||
# If we are in daily mode the time_rule is ignored.
|
time_rule = time_rules.every_minute()
|
||||||
time_rules.every_minute())
|
else:
|
||||||
|
# use provided time rule or default to every minute or 5 minutes
|
||||||
|
# based on desired data frequency.
|
||||||
|
time_rule = time_rule or (time_rules.every_5_minutes()
|
||||||
|
if freq is '5-minute' else
|
||||||
|
time_rules.every_minute())
|
||||||
|
|
||||||
# Check the type of the algorithm's schedule before pulling calendar
|
# Check the type of the algorithm's schedule before pulling calendar
|
||||||
# Note that the ExchangeTradingSchedule is currently the only
|
# Note that the ExchangeTradingSchedule is currently the only
|
||||||
@@ -1464,7 +1490,7 @@ class TradingAlgorithm(object):
|
|||||||
|
|
||||||
def _calculate_order(self, asset, amount,
|
def _calculate_order(self, asset, amount,
|
||||||
limit_price=None, stop_price=None, style=None):
|
limit_price=None, stop_price=None, style=None):
|
||||||
amount = self.round_order(amount, asset)
|
amount = self.round_order(amount)
|
||||||
|
|
||||||
# Raises a ZiplineError if invalid parameters are detected.
|
# Raises a ZiplineError if invalid parameters are detected.
|
||||||
self.validate_order_params(asset,
|
self.validate_order_params(asset,
|
||||||
@@ -1481,13 +1507,16 @@ class TradingAlgorithm(object):
|
|||||||
return amount, style
|
return amount, style
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def round_order(amount, asset):
|
def round_order(amount):
|
||||||
"""
|
"""
|
||||||
Converts the number of shares to the smallest tradable lot size for
|
Convert number of shares to an integer.
|
||||||
the asset being ordered.
|
|
||||||
|
|
||||||
|
By default, truncates to the integer share count that's either within
|
||||||
|
.0001 of amount or closer to zero.
|
||||||
|
|
||||||
|
E.g. 3.9999 -> 4.0; 5.5 -> 5.0; -5.5 -> -5.0
|
||||||
"""
|
"""
|
||||||
return round_nearest(amount, asset.min_trade_size)
|
return int(round_if_near_integer(amount))
|
||||||
|
|
||||||
def validate_order_params(self,
|
def validate_order_params(self,
|
||||||
asset,
|
asset,
|
||||||
@@ -1523,6 +1552,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):
|
||||||
"""
|
"""
|
||||||
@@ -1794,7 +1824,7 @@ class TradingAlgorithm(object):
|
|||||||
|
|
||||||
@data_frequency.setter
|
@data_frequency.setter
|
||||||
def data_frequency(self, value):
|
def data_frequency(self, value):
|
||||||
assert value in ('daily', 'minute')
|
assert value in ('daily', '5-minute', 'minute')
|
||||||
self.sim_params.data_frequency = value
|
self.sim_params.data_frequency = value
|
||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
|
|||||||
+27
-260
@@ -17,37 +17,34 @@
|
|||||||
"""
|
"""
|
||||||
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 (
|
||||||
Py_EQ,
|
Py_EQ,
|
||||||
Py_NE,
|
Py_NE,
|
||||||
Py_GE,
|
Py_GE,
|
||||||
Py_LE,
|
Py_LE,
|
||||||
Py_GT,
|
Py_GT,
|
||||||
Py_LT,
|
Py_LT,
|
||||||
)
|
)
|
||||||
from cpython cimport bool
|
from cpython cimport bool
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
from datetime import timedelta
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy cimport int64_t
|
from numpy cimport int64_t
|
||||||
import warnings
|
import warnings
|
||||||
cimport numpy as np
|
cimport numpy as np
|
||||||
|
|
||||||
from catalyst.exchange.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
|
|
||||||
|
|
||||||
# IMPORTANT NOTE: You must change this template if you change
|
# IMPORTANT NOTE: You must change this template if you change
|
||||||
# Asset.__reduce__, or else we'll attempt to unpickle an old version of this
|
# Asset.__reduce__, or else we'll attempt to unpickle an old version of this
|
||||||
# class
|
# class
|
||||||
CACHE_FILE_TEMPLATE = '/tmp/.%s-%s.v7.cache'
|
CACHE_FILE_TEMPLATE = '/tmp/.%s-%s.v7.cache'
|
||||||
|
|
||||||
|
|
||||||
cdef class Asset:
|
cdef class Asset:
|
||||||
|
|
||||||
cdef readonly int sid
|
cdef readonly int sid
|
||||||
# Cached hash of self.sid
|
# Cached hash of self.sid
|
||||||
cdef int sid_hash
|
cdef int sid_hash
|
||||||
@@ -62,7 +59,6 @@ cdef class Asset:
|
|||||||
|
|
||||||
cdef readonly object exchange
|
cdef readonly object exchange
|
||||||
cdef readonly object exchange_full
|
cdef readonly object exchange_full
|
||||||
cdef readonly object min_trade_size
|
|
||||||
|
|
||||||
_kwargnames = frozenset({
|
_kwargnames = frozenset({
|
||||||
'sid',
|
'sid',
|
||||||
@@ -74,20 +70,18 @@ cdef class Asset:
|
|||||||
'auto_close_date',
|
'auto_close_date',
|
||||||
'exchange',
|
'exchange',
|
||||||
'exchange_full',
|
'exchange_full',
|
||||||
'min_trade_size',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
int sid, # sid is required
|
int sid, # sid is required
|
||||||
object exchange, # exchange is required
|
object exchange, # exchange is required
|
||||||
object symbol="",
|
object symbol="",
|
||||||
object asset_name="",
|
object asset_name="",
|
||||||
object start_date=None,
|
object start_date=None,
|
||||||
object end_date=None,
|
object end_date=None,
|
||||||
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):
|
|
||||||
|
|
||||||
self.sid = sid
|
self.sid = sid
|
||||||
self.sid_hash = hash(sid)
|
self.sid_hash = hash(sid)
|
||||||
@@ -100,7 +94,6 @@ cdef class Asset:
|
|||||||
self.end_date = end_date
|
self.end_date = end_date
|
||||||
self.first_traded = first_traded
|
self.first_traded = first_traded
|
||||||
self.auto_close_date = auto_close_date
|
self.auto_close_date = auto_close_date
|
||||||
self.min_trade_size = min_trade_size
|
|
||||||
|
|
||||||
def __int__(self):
|
def __int__(self):
|
||||||
return self.sid
|
return self.sid
|
||||||
@@ -155,8 +148,7 @@ cdef class Asset:
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
attrs = ('symbol', 'asset_name', 'exchange',
|
attrs = ('symbol', 'asset_name', 'exchange',
|
||||||
'start_date', 'end_date', 'first_traded', 'auto_close_date',
|
'start_date', 'end_date', 'first_traded', 'auto_close_date')
|
||||||
'min_trade_size')
|
|
||||||
tuples = ((attr, repr(getattr(self, attr, None)))
|
tuples = ((attr, repr(getattr(self, attr, None)))
|
||||||
for attr in attrs)
|
for attr in attrs)
|
||||||
strings = ('%s=%s' % (t[0], t[1]) for t in tuples)
|
strings = ('%s=%s' % (t[0], t[1]) for t in tuples)
|
||||||
@@ -178,8 +170,7 @@ cdef class Asset:
|
|||||||
self.end_date,
|
self.end_date,
|
||||||
self.first_traded,
|
self.first_traded,
|
||||||
self.auto_close_date,
|
self.auto_close_date,
|
||||||
self.exchange_full,
|
self.exchange_full))
|
||||||
self.min_trade_size))
|
|
||||||
|
|
||||||
cpdef to_dict(self):
|
cpdef to_dict(self):
|
||||||
"""
|
"""
|
||||||
@@ -195,7 +186,6 @@ cdef class Asset:
|
|||||||
'auto_close_date': self.auto_close_date,
|
'auto_close_date': self.auto_close_date,
|
||||||
'exchange': self.exchange,
|
'exchange': self.exchange,
|
||||||
'exchange_full': self.exchange_full,
|
'exchange_full': self.exchange_full,
|
||||||
'min_trade_size': self.min_trade_size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -240,11 +230,13 @@ cdef class Asset:
|
|||||||
calendar = get_calendar(self.exchange)
|
calendar = get_calendar(self.exchange)
|
||||||
return calendar.is_open_on_minute(dt_minute)
|
return calendar.is_open_on_minute(dt_minute)
|
||||||
|
|
||||||
|
|
||||||
cdef class Equity(Asset):
|
cdef class Equity(Asset):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
attrs = ('symbol', 'asset_name', 'exchange',
|
attrs = ('symbol', 'asset_name', 'exchange',
|
||||||
'start_date', 'end_date', 'first_traded', 'auto_close_date',
|
'start_date', 'end_date', 'first_traded', 'auto_close_date',
|
||||||
'exchange_full', 'min_trade_size')
|
'exchange_full')
|
||||||
tuples = ((attr, repr(getattr(self, attr, None)))
|
tuples = ((attr, repr(getattr(self, attr, None)))
|
||||||
for attr in attrs)
|
for attr in attrs)
|
||||||
strings = ('%s=%s' % (t[0], t[1]) for t in tuples)
|
strings = ('%s=%s' % (t[0], t[1]) for t in tuples)
|
||||||
@@ -258,8 +250,8 @@ cdef class Equity(Asset):
|
|||||||
"""
|
"""
|
||||||
def __get__(self):
|
def __get__(self):
|
||||||
warnings.warn("The security_start_date property will soon be "
|
warnings.warn("The security_start_date property will soon be "
|
||||||
"retired. Please use the start_date property instead.",
|
"retired. Please use the start_date property instead.",
|
||||||
DeprecationWarning)
|
DeprecationWarning)
|
||||||
return self.start_date
|
return self.start_date
|
||||||
|
|
||||||
property security_end_date:
|
property security_end_date:
|
||||||
@@ -269,8 +261,8 @@ cdef class Equity(Asset):
|
|||||||
"""
|
"""
|
||||||
def __get__(self):
|
def __get__(self):
|
||||||
warnings.warn("The security_end_date property will soon be "
|
warnings.warn("The security_end_date property will soon be "
|
||||||
"retired. Please use the end_date property instead.",
|
"retired. Please use the end_date property instead.",
|
||||||
DeprecationWarning)
|
DeprecationWarning)
|
||||||
return self.end_date
|
return self.end_date
|
||||||
|
|
||||||
property security_name:
|
property security_name:
|
||||||
@@ -280,11 +272,13 @@ cdef class Equity(Asset):
|
|||||||
"""
|
"""
|
||||||
def __get__(self):
|
def __get__(self):
|
||||||
warnings.warn("The security_name property will soon be "
|
warnings.warn("The security_name property will soon be "
|
||||||
"retired. Please use the asset_name property instead.",
|
"retired. Please use the asset_name property instead.",
|
||||||
DeprecationWarning)
|
DeprecationWarning)
|
||||||
return self.asset_name
|
return self.asset_name
|
||||||
|
|
||||||
|
|
||||||
cdef class Future(Asset):
|
cdef class Future(Asset):
|
||||||
|
|
||||||
cdef readonly object root_symbol
|
cdef readonly object root_symbol
|
||||||
cdef readonly object notice_date
|
cdef readonly object notice_date
|
||||||
cdef readonly object expiration_date
|
cdef readonly object expiration_date
|
||||||
@@ -309,8 +303,8 @@ cdef class Future(Asset):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
int sid, # sid is required
|
int sid, # sid is required
|
||||||
object exchange, # exchange is required
|
object exchange, # exchange is required
|
||||||
object symbol="",
|
object symbol="",
|
||||||
object root_symbol="",
|
object root_symbol="",
|
||||||
object asset_name="",
|
object asset_name="",
|
||||||
@@ -394,233 +388,6 @@ cdef class Future(Asset):
|
|||||||
super_dict['multiplier'] = self.multiplier
|
super_dict['multiplier'] = self.multiplier
|
||||||
return super_dict
|
return super_dict
|
||||||
|
|
||||||
cdef class TradingPair(Asset):
|
|
||||||
cdef readonly float leverage
|
|
||||||
cdef readonly object quote_currency
|
|
||||||
cdef readonly object base_currency
|
|
||||||
cdef readonly object end_daily
|
|
||||||
cdef readonly object end_minute
|
|
||||||
cdef readonly object exchange_symbol
|
|
||||||
cdef readonly float maker
|
|
||||||
cdef readonly float taker
|
|
||||||
cdef readonly int trading_state
|
|
||||||
cdef readonly object data_source
|
|
||||||
|
|
||||||
_kwargnames = frozenset({
|
|
||||||
'sid',
|
|
||||||
'symbol',
|
|
||||||
'asset_name',
|
|
||||||
'start_date',
|
|
||||||
'end_date',
|
|
||||||
'first_traded',
|
|
||||||
'auto_close_date',
|
|
||||||
'exchange',
|
|
||||||
'exchange_full',
|
|
||||||
'leverage',
|
|
||||||
'quote_currency',
|
|
||||||
'base_currency',
|
|
||||||
'end_daily',
|
|
||||||
'end_minute',
|
|
||||||
'exchange_symbol',
|
|
||||||
'min_trade_size',
|
|
||||||
'maker',
|
|
||||||
'taker',
|
|
||||||
'trading_state',
|
|
||||||
'data_source'
|
|
||||||
})
|
|
||||||
def __init__(self,
|
|
||||||
object symbol,
|
|
||||||
object exchange,
|
|
||||||
object start_date=None,
|
|
||||||
object asset_name=None,
|
|
||||||
int sid=0,
|
|
||||||
float leverage=1.0,
|
|
||||||
object end_daily=None,
|
|
||||||
object end_minute=None,
|
|
||||||
object end_date=None,
|
|
||||||
object exchange_symbol=None,
|
|
||||||
object first_traded=None,
|
|
||||||
object auto_close_date=None,
|
|
||||||
object exchange_full=None,
|
|
||||||
float min_trade_size=0.0001,
|
|
||||||
float maker=0.0015,
|
|
||||||
float taker=0.0025,
|
|
||||||
int trading_state=0,
|
|
||||||
object data_source='catalyst'):
|
|
||||||
"""
|
|
||||||
Replicates the Asset constructor with some built-in conventions
|
|
||||||
and adds properties for leverage and fees.
|
|
||||||
|
|
||||||
Symbol
|
|
||||||
------
|
|
||||||
Catalyst defines its own set of "universal" symbols to reference
|
|
||||||
trading pairs across exchanges. This is required because exchanges
|
|
||||||
are not adhering to a universal symbolism. For example, Bitfinex
|
|
||||||
uses the BTC symbol for Bitcon while Kraken uses XBT. In addition,
|
|
||||||
pairs are sometimes presented differently. For example, Bitfinex
|
|
||||||
puts the market currency before the base currency without a
|
|
||||||
separator, Bittrex puts the base currency first and uses a dash
|
|
||||||
seperator.
|
|
||||||
|
|
||||||
Here is the Catalyst convention: [Market Currency]_[Base Currency]
|
|
||||||
For example: btc_usd, eth_btc, neo_eth, ltc_eur.
|
|
||||||
|
|
||||||
The symbol for each currency (e.g. btc, eth, ltc) is generally
|
|
||||||
aligned with the Bittrex exchange.
|
|
||||||
|
|
||||||
Sid
|
|
||||||
---
|
|
||||||
The sid of each asset is calculated based on a numeric hash of the
|
|
||||||
universal symbol. This simple approach avoids maintaining a mapping
|
|
||||||
of sids.
|
|
||||||
|
|
||||||
Leverage
|
|
||||||
--------
|
|
||||||
In contrast with equities, crypto exchanges generally assign
|
|
||||||
leverage values to specific trading pairs. Pairs with the
|
|
||||||
highest volume and market cap generally benefit from high leverage.
|
|
||||||
New currencies from ICO generally cannot be leveraged.
|
|
||||||
|
|
||||||
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
|
|
||||||
with 5:1 leverage, only one-fifth of this amount, or $1000, will be
|
|
||||||
tied to the position from your balance. Your remaining balance will
|
|
||||||
be available for opening more positions. If you open this same
|
|
||||||
position with 2:1 leverage, $2,500 of your balance will be tied to
|
|
||||||
the position. If you open with 1:1 leverage, $5,000 of your balance
|
|
||||||
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 exchange:
|
|
||||||
:param start_date:
|
|
||||||
:param asset_name:
|
|
||||||
:param sid:
|
|
||||||
:param leverage:
|
|
||||||
:param end_daily
|
|
||||||
:param end_minute
|
|
||||||
:param end_date:
|
|
||||||
:param exchange_symbol:
|
|
||||||
:param first_traded:
|
|
||||||
:param auto_close_date:
|
|
||||||
:param exchange_full:
|
|
||||||
:param min_trade_size:
|
|
||||||
:param maker:
|
|
||||||
:param taker:
|
|
||||||
:param data_source
|
|
||||||
"""
|
|
||||||
|
|
||||||
symbol = symbol.lower()
|
|
||||||
try:
|
|
||||||
self.base_currency, self.quote_currency = symbol.split('_')
|
|
||||||
except Exception as e:
|
|
||||||
raise InvalidSymbolError(symbol=symbol, error=e)
|
|
||||||
|
|
||||||
if sid == 0 or sid is None:
|
|
||||||
try:
|
|
||||||
sid = get_sid(symbol)
|
|
||||||
except Exception as e:
|
|
||||||
raise SidHashError(symbol=symbol)
|
|
||||||
|
|
||||||
if asset_name is None:
|
|
||||||
asset_name = ' / '.join(symbol.split('_')).upper()
|
|
||||||
|
|
||||||
if start_date is None:
|
|
||||||
start_date = pd.to_datetime('2009-1-1', utc=True)
|
|
||||||
|
|
||||||
if end_date is None:
|
|
||||||
end_date = pd.Timestamp.utcnow() + timedelta(days=365)
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
sid,
|
|
||||||
exchange,
|
|
||||||
symbol=symbol,
|
|
||||||
asset_name=asset_name,
|
|
||||||
start_date=start_date,
|
|
||||||
end_date=end_date,
|
|
||||||
first_traded=first_traded,
|
|
||||||
auto_close_date=auto_close_date,
|
|
||||||
exchange_full=exchange_full,
|
|
||||||
min_trade_size=min_trade_size,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.maker = maker
|
|
||||||
self.taker = taker
|
|
||||||
self.leverage = leverage
|
|
||||||
self.end_daily = end_daily
|
|
||||||
self.end_minute = end_minute
|
|
||||||
self.exchange_symbol = exchange_symbol
|
|
||||||
self.trading_state = trading_state
|
|
||||||
self.data_source = data_source
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \
|
|
||||||
'Introduced On: {start_date}, ' \
|
|
||||||
'Base Currency: {base_currency}, ' \
|
|
||||||
'Quote Currency: {quote_currency}, ' \
|
|
||||||
'Exchange Leverage: {leverage}, ' \
|
|
||||||
'Minimum Trade Size: {min_trade_size} ' \
|
|
||||||
'Last daily ingestion: {end_daily} ' \
|
|
||||||
'Last minutely ingestion: {end_minute}'.format(
|
|
||||||
symbol=self.symbol,
|
|
||||||
sid=self.sid,
|
|
||||||
exchange=self.exchange,
|
|
||||||
start_date=self.start_date,
|
|
||||||
quote_currency=self.quote_currency,
|
|
||||||
base_currency=self.base_currency,
|
|
||||||
leverage=self.leverage,
|
|
||||||
min_trade_size=self.min_trade_size,
|
|
||||||
end_daily=self.end_daily,
|
|
||||||
end_minute=self.end_minute
|
|
||||||
)
|
|
||||||
|
|
||||||
cpdef to_dict(self):
|
|
||||||
"""
|
|
||||||
Convert to a python dict.
|
|
||||||
"""
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
Function used by pickle to determine how to serialize/deserialize this
|
|
||||||
class. Should return a tuple whose first element is self.__class__,
|
|
||||||
and whose second element is a tuple of all the attributes that should
|
|
||||||
be serialized/deserialized during pickling.
|
|
||||||
"""
|
|
||||||
return (self.__class__, (self.symbol,
|
|
||||||
self.exchange,
|
|
||||||
self.start_date,
|
|
||||||
self.asset_name,
|
|
||||||
self.sid,
|
|
||||||
self.leverage,
|
|
||||||
self.end_date,
|
|
||||||
self.first_traded,
|
|
||||||
self.auto_close_date,
|
|
||||||
self.exchange_full,
|
|
||||||
self.min_trade_size))
|
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ equities = sa.Table(
|
|||||||
sa.Column('first_traded', sa.Integer),
|
sa.Column('first_traded', sa.Integer),
|
||||||
sa.Column('auto_close_date', sa.Integer),
|
sa.Column('auto_close_date', sa.Integer),
|
||||||
sa.Column('exchange', sa.Text),
|
sa.Column('exchange', sa.Text),
|
||||||
sa.Column('exchange_full', sa.Text),
|
sa.Column('exchange_full', sa.Text)
|
||||||
sa.Column('min_trade_size', sa.Float)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
equity_symbol_mappings = sa.Table(
|
equity_symbol_mappings = sa.Table(
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ _equities_defaults = {
|
|||||||
'exchange': None,
|
'exchange': None,
|
||||||
# optional, something like "New York Stock Exchange"
|
# optional, something like "New York Stock Exchange"
|
||||||
'exchange_full': None,
|
'exchange_full': None,
|
||||||
'min_trade_size': 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Default values for the futures DataFrame
|
# Default values for the futures DataFrame
|
||||||
@@ -391,8 +390,6 @@ class AssetDBWriter(object):
|
|||||||
The date on which to close any positions in this asset.
|
The date on which to close any positions in this asset.
|
||||||
exchange : str
|
exchange : str
|
||||||
The exchange where this asset is traded.
|
The exchange where this asset is traded.
|
||||||
min_trade_size: float, optional
|
|
||||||
The minimum denomination this asset can be traded.
|
|
||||||
|
|
||||||
The index of this dataframe should contain the sids.
|
The index of this dataframe should contain the sids.
|
||||||
futures : pd.DataFrame, optional
|
futures : pd.DataFrame, optional
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ 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
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = Logger('assets.py')
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# -*- 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))
|
|
||||||
LOG_LEVEL = logbook.DEBUG
|
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
AUTO_INGEST = False
|
|
||||||
+82
-314
@@ -1,22 +1,22 @@
|
|||||||
import json, time, csv
|
import json, time, csv
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import os, time, shutil, requests, logbook
|
import os
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename
|
import time
|
||||||
|
import requests
|
||||||
|
import logbook
|
||||||
|
|
||||||
|
DT_START = time.mktime(datetime(2010, 1, 1, 0, 0).timetuple())
|
||||||
DT_START = int(time.mktime(datetime(2010, 1, 1, 0, 0).timetuple()))
|
CSV_OUT_FOLDER = '/var/tmp/catalyst/data/poloniex/'
|
||||||
DT_END = pd.to_datetime('today').value // 10 ** 9
|
|
||||||
CSV_OUT_FOLDER = os.environ.get('CSV_OUT_FOLDER', '/efs/exchanges/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 = []
|
||||||
@@ -26,15 +26,10 @@ class PoloniexCurator(object):
|
|||||||
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: {}'.format(
|
log.error('Failed to create data folder: %s' % CSV_OUT_FOLDER)
|
||||||
CSV_OUT_FOLDER))
|
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -50,327 +45,100 @@ class PoloniexCurator(object):
|
|||||||
self.currency_pairs.append(ticker)
|
self.currency_pairs.append(ticker)
|
||||||
self.currency_pairs.sort()
|
self.currency_pairs.sort()
|
||||||
|
|
||||||
log.debug('Currency pairs retrieved successfully: {}'.format(
|
log.debug('Currency pairs retrieved successfully: %d' % (len(self.currency_pairs)))
|
||||||
len(self.currency_pairs)
|
|
||||||
))
|
|
||||||
|
|
||||||
|
def _get_start_date(self, csv_fn):
|
||||||
|
''' Function returns latest appended date, if the file has been previously written
|
||||||
def _retrieve_tradeID_date(self, row):
|
the last line is an empty one, so we have to read the second to last line
|
||||||
'''
|
|
||||||
Helper function that reads tradeID and date fields from CSV readline
|
|
||||||
'''
|
|
||||||
tId = int(row.split(',')[0])
|
|
||||||
d = pd.to_datetime(row.split(',')[1],
|
|
||||||
infer_datetime_format=True).value // 10 ** 9
|
|
||||||
return tId, d
|
|
||||||
|
|
||||||
|
|
||||||
def retrieve_trade_history(self, currencyPair, start=DT_START,
|
|
||||||
end=DT_END, temp=None):
|
|
||||||
'''
|
|
||||||
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.
|
|
||||||
|
|
||||||
This function is called recursively to work around the
|
|
||||||
limitations imposed by the provider API.
|
|
||||||
'''
|
|
||||||
csv_fn = CSV_OUT_FOLDER + 'crypto_trades-' + currencyPair + '.csv'
|
|
||||||
|
|
||||||
'''
|
|
||||||
Check what data we already have on disk, reading first and last
|
|
||||||
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) # First check file is not zero size
|
||||||
if(f.tell() > 2): # Check file size is not 0
|
if(f.tell() > 2):
|
||||||
f.seek(0) # Go to start to read
|
f.seek(-2, os.SEEK_END) # Jump to the second last byte.
|
||||||
last_tradeID, end_file = self._retrieve_tradeID_date(f.readline())
|
|
||||||
f.seek(-2, os.SEEK_END) # Jump to the 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.
|
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
|
||||||
first_tradeID, start_file = self._retrieve_tradeID_date(f.readline())
|
lastrow = f.readline()
|
||||||
|
return int(lastrow.split(',')[0]) + 300
|
||||||
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
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Error opening file: {}'.format(csv_fn))
|
log.error('Error opening file: %s' % csv_fn)
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
|
|
||||||
'''
|
return DT_START
|
||||||
Poloniex API limits querying TradeHistory to intervals smaller
|
|
||||||
than 1 month, so we make sure that start date is never more than
|
|
||||||
1 month apart from end date
|
|
||||||
'''
|
|
||||||
if( end - start > 2419200 ): # 60s/min * 60min/hr * 24hr/day * 28days
|
|
||||||
newstart = end - 2419200
|
|
||||||
else:
|
|
||||||
newstart = start
|
|
||||||
|
|
||||||
log.debug('{}: Retrieving from {} to {}\t {} - {}'.format(
|
def get_data(self, currencyPair, start, end=9999999999, period=300):
|
||||||
currencyPair, str(newstart), str(end),
|
url = self._api_path + 'command=returnChartData¤cyPair=' + currencyPair + '&start=' + str(start) + '&end=' + str(end) + '&period=' + str(period)
|
||||||
time.ctime(newstart), time.ctime(end)))
|
|
||||||
|
|
||||||
url = '{path}command=returnTradeHistory¤cyPair={pair}' \
|
try:
|
||||||
'&start={start}&end={end}'.format(
|
response = requests.get(url)
|
||||||
path = self._api_path,
|
except Exception as e:
|
||||||
pair = currencyPair,
|
log.error('Failed to retrieve candlestick chart data for %s' % currencyPair)
|
||||||
start = str(newstart),
|
log.exception(e)
|
||||||
end = str(end)
|
|
||||||
)
|
|
||||||
print url
|
|
||||||
|
|
||||||
attempts = 0
|
|
||||||
success = 0
|
|
||||||
while attempts < CONN_RETRIES:
|
|
||||||
try:
|
|
||||||
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
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
'''
|
'''
|
||||||
If we get to transactionId == 1, and we already have that on
|
Pulls latest data for a single pair
|
||||||
disk, we got to the end of TradeHistory for this coin.
|
'''
|
||||||
'''
|
def append_data_single_pair(self, currencyPair, repeat=0):
|
||||||
if('first_tradeID' in locals()
|
log.debug('Getting data for %s' % currencyPair)
|
||||||
and response.json()[-1]['tradeID'] == first_tradeID):
|
csv_fn = CSV_OUT_FOLDER + 'crypto_prices-' + currencyPair + '.csv'
|
||||||
return
|
start = self._get_start_date(csv_fn)
|
||||||
|
# Only fetch data if more than 5min have passed since last fetch
|
||||||
|
if (time.time() > start):
|
||||||
|
data = self.get_data(currencyPair, start)
|
||||||
|
if data is not None:
|
||||||
|
try:
|
||||||
|
with open(csv_fn, 'ab') as csvfile:
|
||||||
|
csvwriter = csv.writer(csvfile)
|
||||||
|
for item in data:
|
||||||
|
if item['date'] == 0:
|
||||||
|
continue
|
||||||
|
csvwriter.writerow([
|
||||||
|
item['date'],
|
||||||
|
item['open'],
|
||||||
|
item['high'],
|
||||||
|
item['low'],
|
||||||
|
item['close'],
|
||||||
|
item['volume'],
|
||||||
|
])
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Error opening %s' % csv_fn)
|
||||||
|
log.exception(e)
|
||||||
|
elif (repeat < CONN_RETRIES):
|
||||||
|
log.debug('Retrying: attemt %d' % (repeat+1) )
|
||||||
|
self.append_data_single_pair(currencyPair, repeat + 1)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
There are primarily two scenarios:
|
Pulls latest data for all currency pairs
|
||||||
a) There is newer data available that we need to add at
|
'''
|
||||||
the beginning of the file. We'll retrieve all what we
|
def append_data(self):
|
||||||
need until we get to what we already have, writing it
|
for currencyPair in self.currency_pairs:
|
||||||
to a temporary file; and we will write that at the
|
self.append_data_single_pair(currencyPair)
|
||||||
beginning of our existing file.
|
# Rate limit is 6 calls per second, sleep 1sec/6 to be safe
|
||||||
b) We are going back in time, appending at the end of
|
time.sleep(0.17)
|
||||||
our existing TradeHistory until the first transaction
|
|
||||||
for this currencyPair
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
if( 'end_file' in locals() and end_file + 3600 < end):
|
|
||||||
if (temp is None):
|
|
||||||
temp = os.tmpfile()
|
|
||||||
tempcsv = csv.writer(temp)
|
|
||||||
for item in response.json():
|
|
||||||
if( item['tradeID'] <= last_tradeID ):
|
|
||||||
continue
|
|
||||||
tempcsv.writerow([
|
|
||||||
item['tradeID'],
|
|
||||||
item['date'],
|
|
||||||
item['type'],
|
|
||||||
item['rate'],
|
|
||||||
item['amount'],
|
|
||||||
item['total'],
|
|
||||||
item['globalTradeID']
|
|
||||||
])
|
|
||||||
if( response.json()[-1]['tradeID'] > last_tradeID ):
|
|
||||||
end = pd.to_datetime( response.json()[-1]['date'],
|
|
||||||
infer_datetime_format=True).value // 10 ** 9
|
|
||||||
self.retrieve_trade_history(currencyPair, start,
|
|
||||||
end, temp=temp)
|
|
||||||
else:
|
|
||||||
with open(csv_fn,'rb+') as f:
|
|
||||||
shutil.copyfileobj(f,temp)
|
|
||||||
f.seek(0)
|
|
||||||
temp.seek(0)
|
|
||||||
shutil.copyfileobj(temp,f)
|
|
||||||
temp.close()
|
|
||||||
end = start_file
|
|
||||||
else:
|
|
||||||
with open(csv_fn, 'ab') as csvfile:
|
|
||||||
csvwriter = csv.writer(csvfile)
|
|
||||||
for item in response.json():
|
|
||||||
if( 'first_tradeID' in locals()
|
|
||||||
and item['tradeID'] >= first_tradeID ):
|
|
||||||
continue
|
|
||||||
csvwriter.writerow([
|
|
||||||
item['tradeID'],
|
|
||||||
item['date'],
|
|
||||||
item['type'],
|
|
||||||
item['rate'],
|
|
||||||
item['amount'],
|
|
||||||
item['total'],
|
|
||||||
item['globalTradeID']
|
|
||||||
])
|
|
||||||
end = pd.to_datetime(response.json()[-1]['date'],
|
|
||||||
infer_datetime_format=True).value // 10 ** 9
|
|
||||||
|
|
||||||
except Exception as e:
|
'''
|
||||||
log.error('Error opening {}'.format(csv_fn))
|
Returns a data frame for all pairs, or for the requests currency pair.
|
||||||
log.exception(e)
|
Makes sure data is up to date
|
||||||
|
'''
|
||||||
|
def to_dataframe(self, start, end, currencyPair=None):
|
||||||
|
csv_fn = CSV_OUT_FOLDER + 'crypto_prices-' + currencyPair + '.csv'
|
||||||
|
last_date = self._get_start_date(csv_fn)
|
||||||
|
if last_date + 300 < end or not os.path.exists(csv_fn):
|
||||||
|
# get latest data
|
||||||
|
self.append_data_single_pair(currencyPair)
|
||||||
|
|
||||||
'''
|
# CSV holds the latest snapshot
|
||||||
If we got here, we aren't done yet. Call recursively with
|
df = pd.read_csv(csv_fn, names=['date', 'open', 'high', 'low', 'close', 'volume'])
|
||||||
'end' times that go sequentially back in time.
|
df['date']=pd.to_datetime(df['date'],unit='s')
|
||||||
'''
|
|
||||||
self.retrieve_trade_history(currencyPair, start, end)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def generate_ohlcv(self, df):
|
|
||||||
'''
|
|
||||||
Generates OHLCV dataframe from a dataframe containing all TradeHistory
|
|
||||||
by resampling with 1-minute period
|
|
||||||
'''
|
|
||||||
df.set_index('date', inplace=True) # Index by date
|
|
||||||
vol = df['total'].to_frame('volume') # set Vol aside
|
|
||||||
df.drop('total', axis=1, inplace=True) # Drop volume data
|
|
||||||
ohlc = df.resample('T').ohlc() # Resample OHLC 1min
|
|
||||||
ohlc.columns = ohlc.columns.map(lambda t: t[1]) # Raname columns by dropping 'rate'
|
|
||||||
closes = ohlc['close'].fillna(method='pad') # Pad fwd missing '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
|
|
||||||
ohlcv = pd.concat([ohlc,vol], axis=1) # Concatenate OHLC + Vol
|
|
||||||
return ohlcv
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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_1min = CSV_OUT_FOLDER + 'crypto_1min-' + currencyPair + '.csv'
|
|
||||||
if( os.path.getmtime(csv_1min) > time.time() - 7200 ):
|
|
||||||
log.debug(currencyPair+': 1min data file already up to date. '
|
|
||||||
'Delete the file if you want to rebuild it.')
|
|
||||||
else:
|
|
||||||
df = pd.read_csv(csv_trades,
|
|
||||||
names=['tradeID',
|
|
||||||
'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)
|
|
||||||
ohlcv = self.generate_ohlcv(df)
|
|
||||||
try:
|
|
||||||
with open(csv_1min, 'w') as csvfile:
|
|
||||||
csvwriter = csv.writer(csvfile)
|
|
||||||
for item in ohlcv.itertuples():
|
|
||||||
if item.Index == 0:
|
|
||||||
continue
|
|
||||||
csvwriter.writerow([
|
|
||||||
item.Index.value // 10 ** 9,
|
|
||||||
item.open,
|
|
||||||
item.high,
|
|
||||||
item.low,
|
|
||||||
item.close,
|
|
||||||
item.volume,
|
|
||||||
])
|
|
||||||
except Exception as e:
|
|
||||||
log.error('Error opening {}'.format(csv_fn))
|
|
||||||
log.exception(e)
|
|
||||||
log.debug('{}: Generated 1min OHLCV data.'.format(currencyPair))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def onemin_to_dataframe(self, currencyPair, start, end):
|
|
||||||
'''
|
|
||||||
Returns a data frame for a given currencyPair from data on disk
|
|
||||||
'''
|
|
||||||
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]
|
|
||||||
|
|
||||||
|
|
||||||
def generate_symbols_json(self, filename=None):
|
|
||||||
'''
|
|
||||||
Generates a symbols.json file with corresponding start_date
|
|
||||||
for each currencyPair
|
|
||||||
'''
|
|
||||||
symbol_map = {}
|
|
||||||
|
|
||||||
if(filename is None):
|
|
||||||
filename = get_exchange_symbols_filename('poloniex')
|
|
||||||
|
|
||||||
with open(filename, 'w') as symbols:
|
|
||||||
for currencyPair in self.currency_pairs:
|
|
||||||
start = None
|
|
||||||
csv_fn = '{}crypto_trades-{}.csv'.format(
|
|
||||||
CSV_OUT_FOLDER, currencyPair)
|
|
||||||
with open(csv_fn, 'r') as f:
|
|
||||||
f.seek(0, os.SEEK_END)
|
|
||||||
if(f.tell() > 2): # Check file size is not 0
|
|
||||||
f.seek(-2, os.SEEK_END) # Jump to 2nd last byte
|
|
||||||
while f.read(1) != b"\n": # Until EOL is found...
|
|
||||||
f.seek(-2, os.SEEK_CUR) # ...jump back the read byte plus one more.
|
|
||||||
start = pd.to_datetime( f.readline().split(',')[1],
|
|
||||||
infer_datetime_format=True)
|
|
||||||
|
|
||||||
if(start is None):
|
|
||||||
start = time.gmtime()
|
|
||||||
base, market = currencyPair.lower().split('_')
|
|
||||||
symbol = '{market}_{base}'.format( market=market, base=base )
|
|
||||||
symbol_map[currencyPair] = dict(
|
|
||||||
symbol = symbol,
|
|
||||||
start_date = start.strftime("%Y-%m-%d")
|
|
||||||
)
|
|
||||||
json.dump(symbol_map, symbols, sort_keys=True, indent=2,
|
|
||||||
separators=(',',':'))
|
|
||||||
|
|
||||||
|
return df[datetime.fromtimestamp(start):datetime.fromtimestamp(end-1)]
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
pc = PoloniexCurator()
|
pc = PoloniexCurator()
|
||||||
pc.get_currency_pairs()
|
pc.get_currency_pairs()
|
||||||
#pc.generate_symbols_json()
|
pc.append_data()
|
||||||
|
|
||||||
for currencyPair in pc.currency_pairs:
|
|
||||||
pc.retrieve_trade_history(currencyPair)
|
|
||||||
log.debug('{} up to date.'.format(currencyPair))
|
|
||||||
pc.write_ohlcv_file(currencyPair)
|
|
||||||
|
|
||||||
|
|||||||
@@ -215,13 +215,13 @@ cpdef _read_bcolz_data(ctable_t table,
|
|||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if column_name in ['open', 'high', 'low', 'close', 'volume']:
|
if column_name in ['open', 'high', 'low', 'close']:
|
||||||
where_nan = (outbuf == 0)
|
where_nan = (outbuf == 0)
|
||||||
outbuf_as_float = outbuf.astype(float64) * .000000001
|
outbuf_as_float = outbuf.astype(float64) * .000001
|
||||||
outbuf_as_float[where_nan] = NAN
|
outbuf_as_float[where_nan] = NAN
|
||||||
results.append(outbuf_as_float)
|
results.append(outbuf_as_float)
|
||||||
elif column_name in ['volume']:
|
elif column_name != 'volume':
|
||||||
results.append(outbuf.astype(float64) * .000000001)
|
results.append(outbuf.astype(uint32))
|
||||||
else:
|
else:
|
||||||
results.append(outbuf)
|
results.append(outbuf)
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ def minute_value(ndarray[long_t, ndim=1] market_opens,
|
|||||||
|
|
||||||
return market_opens[q] + r
|
return market_opens[q] + r
|
||||||
|
|
||||||
|
@cython.cdivision(True)
|
||||||
|
def five_minute_value(ndarray[long_t, ndim=1] market_opens,
|
||||||
|
Py_ssize_t pos,
|
||||||
|
short five_minutes_per_day):
|
||||||
|
|
||||||
|
cdef short q, r
|
||||||
|
q = cython.cdiv(pos, five_minutes_per_day)
|
||||||
|
r = cython.cmod(pos, five_minutes_per_day)
|
||||||
|
|
||||||
|
return market_opens[q] + r
|
||||||
|
|
||||||
def find_position_of_minute(ndarray[long_t, ndim=1] market_opens,
|
def find_position_of_minute(ndarray[long_t, ndim=1] market_opens,
|
||||||
ndarray[long_t, ndim=1] market_closes,
|
ndarray[long_t, ndim=1] market_closes,
|
||||||
long_t minute_val,
|
long_t minute_val,
|
||||||
@@ -88,6 +99,26 @@ def find_position_of_minute(ndarray[long_t, ndim=1] market_opens,
|
|||||||
|
|
||||||
return (market_open_loc * minutes_per_day) + delta
|
return (market_open_loc * minutes_per_day) + delta
|
||||||
|
|
||||||
|
def find_position_of_five_minute(ndarray[long_t, ndim=1] market_opens,
|
||||||
|
ndarray[long_t, ndim=1] market_closes,
|
||||||
|
long_t five_minute_val,
|
||||||
|
short five_minutes_per_day,
|
||||||
|
bool forward_fill):
|
||||||
|
|
||||||
|
cdef Py_ssize_t market_open_loc, market_open, delta
|
||||||
|
|
||||||
|
market_open_loc = \
|
||||||
|
searchsorted(market_opens, five_minute_val, side='right') - 1
|
||||||
|
market_open = market_opens[market_open_loc]
|
||||||
|
market_close = market_closes[market_open_loc]
|
||||||
|
|
||||||
|
if not forward_fill and ((five_minute_val - market_open) >= five_minutes_per_day):
|
||||||
|
raise ValueError("Given five minutes is not between an open and a close")
|
||||||
|
|
||||||
|
delta = int_min(five_minute_val - market_open, market_close - market_open)
|
||||||
|
|
||||||
|
return (market_open_loc * five_minutes_per_day) + delta
|
||||||
|
|
||||||
def find_last_traded_position_internal(
|
def find_last_traded_position_internal(
|
||||||
ndarray[long_t, ndim=1] market_opens,
|
ndarray[long_t, ndim=1] market_opens,
|
||||||
ndarray[long_t, ndim=1] market_closes,
|
ndarray[long_t, ndim=1] market_closes,
|
||||||
@@ -158,3 +189,50 @@ def find_last_traded_position_internal(
|
|||||||
# found a trade event
|
# found a trade event
|
||||||
return -1
|
return -1
|
||||||
|
|
||||||
|
def find_last_traded_five_minute_position_internal(
|
||||||
|
ndarray[long_t, ndim=1] market_opens,
|
||||||
|
ndarray[long_t, ndim=1] market_closes,
|
||||||
|
long_t end_five_minute,
|
||||||
|
long_t start_five_minute,
|
||||||
|
volumes,
|
||||||
|
short five_minutes_per_day):
|
||||||
|
cdef Py_ssize_t minute_pos, current_minute, q
|
||||||
|
|
||||||
|
five_minute_pos = int_min(
|
||||||
|
find_position_of_five_minute(
|
||||||
|
market_opens,
|
||||||
|
market_closes,
|
||||||
|
end_five_minute,
|
||||||
|
five_minutes_per_day,
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
len(volumes) - 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
while five_minute_pos >= 0:
|
||||||
|
current_five_minute = five_minute_value(
|
||||||
|
market_opens, five_minute_pos, five_minutes_per_day
|
||||||
|
)
|
||||||
|
|
||||||
|
q = cython.cdiv(five_minute_pos, five_minutes_per_day)
|
||||||
|
if current_five_minute > market_closes[q]:
|
||||||
|
five_minute_pos = find_position_of_five_minute(
|
||||||
|
market_opens,
|
||||||
|
market_closes,
|
||||||
|
market_closes[q],
|
||||||
|
five_minutes_per_day,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current_five_minute < start_five_minute:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
if volumes[five_minute_pos] != 0:
|
||||||
|
return five_minute_pos
|
||||||
|
|
||||||
|
five_minute_pos -= 1
|
||||||
|
|
||||||
|
# we've gone to the beginning of this asset's range, and still haven't
|
||||||
|
# found a trade event
|
||||||
|
return -1
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ 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__, level=LOG_LEVEL)
|
log = logbook.Logger(__name__)
|
||||||
|
|
||||||
DEFAULT_RETRIES = 5
|
DEFAULT_RETRIES = 5
|
||||||
|
|
||||||
@@ -62,6 +60,10 @@ class BaseBundle(object):
|
|||||||
def minutes_per_day(self):
|
def minutes_per_day(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@lazyval
|
||||||
|
def five_minutes_per_day(self):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
def frequencies(self):
|
def frequencies(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@@ -113,6 +115,7 @@ class BaseBundle(object):
|
|||||||
environ,
|
environ,
|
||||||
asset_db_writer,
|
asset_db_writer,
|
||||||
minute_bar_writer,
|
minute_bar_writer,
|
||||||
|
five_minute_bar_writer,
|
||||||
daily_bar_writer,
|
daily_bar_writer,
|
||||||
adjustment_writer,
|
adjustment_writer,
|
||||||
calendar,
|
calendar,
|
||||||
@@ -159,7 +162,7 @@ class BaseBundle(object):
|
|||||||
|
|
||||||
# Post-process metadata using cached symbol frames, and write to
|
# Post-process metadata using cached symbol frames, and write to
|
||||||
# disk. This metadata must be written before any attempt to write
|
# disk. This metadata must be written before any attempt to write
|
||||||
# minute data.
|
# either minute or 5-minute data.
|
||||||
metadata = self._post_process_metadata(
|
metadata = self._post_process_metadata(
|
||||||
raw_metadata,
|
raw_metadata,
|
||||||
cache,
|
cache,
|
||||||
@@ -167,6 +170,26 @@ class BaseBundle(object):
|
|||||||
)
|
)
|
||||||
asset_db_writer.write(metadata)
|
asset_db_writer.write(metadata)
|
||||||
|
|
||||||
|
# Compile 5-minute symbol data if bundle supports 5-minute mode and
|
||||||
|
# persist the dataset to disk.
|
||||||
|
'''
|
||||||
|
if '5-minute' in self.frequencies:
|
||||||
|
five_minute_bar_writer.write(
|
||||||
|
self._fetch_symbol_iter(
|
||||||
|
api_key,
|
||||||
|
cache,
|
||||||
|
symbol_map,
|
||||||
|
calendar,
|
||||||
|
start_session,
|
||||||
|
end_session,
|
||||||
|
'5-minute',
|
||||||
|
retries,
|
||||||
|
),
|
||||||
|
length=len(symbol_map),
|
||||||
|
show_progress=show_progress,
|
||||||
|
)
|
||||||
|
'''
|
||||||
|
|
||||||
# Compile minute symbol data if bundle supports minute mode and
|
# Compile minute symbol data if bundle supports minute mode and
|
||||||
# persist the dataset to disk.
|
# persist the dataset to disk.
|
||||||
if 'minute' in self.frequencies:
|
if 'minute' in self.frequencies:
|
||||||
@@ -468,7 +491,7 @@ 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')
|
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[
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ class BasePricingBundle(BaseBundle):
|
|||||||
('start_date', 'datetime64[ns]'),
|
('start_date', 'datetime64[ns]'),
|
||||||
('end_date', 'datetime64[ns]'),
|
('end_date', 'datetime64[ns]'),
|
||||||
('ac_date', 'datetime64[ns]'),
|
('ac_date', 'datetime64[ns]'),
|
||||||
('min_trade_size', 'float'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
@@ -47,6 +46,10 @@ class BaseCryptoPricingBundle(BasePricingBundle):
|
|||||||
def minutes_per_day(self):
|
def minutes_per_day(self):
|
||||||
return 1440
|
return 1440
|
||||||
|
|
||||||
|
@lazyval
|
||||||
|
def five_minutes_per_day(self):
|
||||||
|
return 288
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def splits(self):
|
def splits(self):
|
||||||
return []
|
return []
|
||||||
@@ -64,6 +67,10 @@ class BaseEquityPricingBundle(BasePricingBundle):
|
|||||||
def minutes_per_day(self):
|
def minutes_per_day(self):
|
||||||
return 390
|
return 390
|
||||||
|
|
||||||
|
@lazyval
|
||||||
|
def five_minutes_per_day(self):
|
||||||
|
return 78
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def splits(self):
|
def splits(self):
|
||||||
return self._splits
|
return self._splits
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ from ..us_equity_pricing import (
|
|||||||
SQLiteAdjustmentReader,
|
SQLiteAdjustmentReader,
|
||||||
SQLiteAdjustmentWriter,
|
SQLiteAdjustmentWriter,
|
||||||
)
|
)
|
||||||
|
from ..five_minute_bars import (
|
||||||
|
BcolzFiveMinuteBarReader,
|
||||||
|
BcolzFiveMinuteBarWriter,
|
||||||
|
)
|
||||||
from ..minute_bars import (
|
from ..minute_bars import (
|
||||||
BcolzMinuteBarReader,
|
BcolzMinuteBarReader,
|
||||||
BcolzMinuteBarWriter,
|
BcolzMinuteBarWriter,
|
||||||
@@ -50,6 +54,11 @@ def minute_path(bundle_name, timestr, environ=None):
|
|||||||
environ=environ,
|
environ=environ,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def five_minute_path(bundle_name, timestr, environ=None):
|
||||||
|
return pth.data_path(
|
||||||
|
five_minute_relative(bundle_name, timestr, environ),
|
||||||
|
environ=environ,
|
||||||
|
)
|
||||||
|
|
||||||
def daily_path(bundle_name, timestr, environ=None):
|
def daily_path(bundle_name, timestr, environ=None):
|
||||||
return pth.data_path(
|
return pth.data_path(
|
||||||
@@ -83,6 +92,8 @@ def cache_relative(bundle_name, timestr, environ=None):
|
|||||||
def daily_relative(bundle_name, timestr, environ=None):
|
def daily_relative(bundle_name, timestr, environ=None):
|
||||||
return bundle_name, timestr, 'daily_equities.bcolz'
|
return bundle_name, timestr, 'daily_equities.bcolz'
|
||||||
|
|
||||||
|
def five_minute_relative(bundle_name, timestr, environ=None):
|
||||||
|
return bundle_name, timestr, 'five_minute.bcolz'
|
||||||
|
|
||||||
def minute_relative(bundle_name, timestr, environ=None):
|
def minute_relative(bundle_name, timestr, environ=None):
|
||||||
return bundle_name, timestr, 'minute_equities.bcolz'
|
return bundle_name, timestr, 'minute_equities.bcolz'
|
||||||
@@ -195,13 +206,14 @@ RegisteredBundle = namedtuple(
|
|||||||
'start_session',
|
'start_session',
|
||||||
'end_session',
|
'end_session',
|
||||||
'minutes_per_day',
|
'minutes_per_day',
|
||||||
|
'five_minutes_per_day',
|
||||||
'ingest',
|
'ingest',
|
||||||
'create_writers']
|
'create_writers']
|
||||||
)
|
)
|
||||||
|
|
||||||
BundleData = namedtuple(
|
BundleData = namedtuple(
|
||||||
'BundleData',
|
'BundleData',
|
||||||
'asset_finder minute_bar_reader daily_bar_reader '
|
'asset_finder minute_bar_reader five_minute_bar_reader daily_bar_reader '
|
||||||
'adjustment_reader',
|
'adjustment_reader',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -291,6 +303,7 @@ def _make_bundle_core():
|
|||||||
bundle.ingest,
|
bundle.ingest,
|
||||||
calendar_name=bundle.calendar_name,
|
calendar_name=bundle.calendar_name,
|
||||||
minutes_per_day=bundle.minutes_per_day,
|
minutes_per_day=bundle.minutes_per_day,
|
||||||
|
five_minutes_per_day=bundle.five_minutes_per_day,
|
||||||
start_session=start_session,
|
start_session=start_session,
|
||||||
end_session=end_session,
|
end_session=end_session,
|
||||||
create_writers=create_writers,
|
create_writers=create_writers,
|
||||||
@@ -303,6 +316,7 @@ def _make_bundle_core():
|
|||||||
start_session=None,
|
start_session=None,
|
||||||
end_session=None,
|
end_session=None,
|
||||||
minutes_per_day=1440,
|
minutes_per_day=1440,
|
||||||
|
five_minutes_per_day=288,
|
||||||
create_writers=True):
|
create_writers=True):
|
||||||
"""Register a data bundle ingest function.
|
"""Register a data bundle ingest function.
|
||||||
|
|
||||||
@@ -383,6 +397,7 @@ def _make_bundle_core():
|
|||||||
start_session=start_session,
|
start_session=start_session,
|
||||||
end_session=end_session,
|
end_session=end_session,
|
||||||
minutes_per_day=minutes_per_day,
|
minutes_per_day=minutes_per_day,
|
||||||
|
five_minutes_per_day=five_minutes_per_day,
|
||||||
ingest=f,
|
ingest=f,
|
||||||
create_writers=create_writers,
|
create_writers=create_writers,
|
||||||
)
|
)
|
||||||
@@ -481,6 +496,16 @@ def _make_bundle_core():
|
|||||||
# that it can compute the adjustment ratios for the dividends.
|
# that it can compute the adjustment ratios for the dividends.
|
||||||
daily_bar_writer.write(())
|
daily_bar_writer.write(())
|
||||||
|
|
||||||
|
five_minute_bar_writer = BcolzFiveMinuteBarWriter(
|
||||||
|
wd.ensure_dir(*five_minute_relative(
|
||||||
|
name, timestr, environ=environ)
|
||||||
|
),
|
||||||
|
calendar,
|
||||||
|
start_session,
|
||||||
|
end_session,
|
||||||
|
five_minutes_per_day=bundle.five_minutes_per_day,
|
||||||
|
)
|
||||||
|
|
||||||
minute_bar_writer = BcolzMinuteBarWriter(
|
minute_bar_writer = BcolzMinuteBarWriter(
|
||||||
wd.ensure_dir(*minute_relative(
|
wd.ensure_dir(*minute_relative(
|
||||||
name, timestr, environ=environ)
|
name, timestr, environ=environ)
|
||||||
@@ -507,6 +532,7 @@ def _make_bundle_core():
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
daily_bar_writer = None
|
daily_bar_writer = None
|
||||||
|
five_minute_bar_writer = None
|
||||||
minute_bar_writer = None
|
minute_bar_writer = None
|
||||||
asset_db_writer = None
|
asset_db_writer = None
|
||||||
adjustment_db_writer = None
|
adjustment_db_writer = None
|
||||||
@@ -518,6 +544,7 @@ def _make_bundle_core():
|
|||||||
environ,
|
environ,
|
||||||
asset_db_writer,
|
asset_db_writer,
|
||||||
minute_bar_writer,
|
minute_bar_writer,
|
||||||
|
five_minute_bar_writer,
|
||||||
daily_bar_writer,
|
daily_bar_writer,
|
||||||
adjustment_db_writer,
|
adjustment_db_writer,
|
||||||
calendar,
|
calendar,
|
||||||
@@ -604,6 +631,9 @@ def _make_bundle_core():
|
|||||||
minute_bar_reader=BcolzMinuteBarReader(
|
minute_bar_reader=BcolzMinuteBarReader(
|
||||||
minute_path(name, timestr, environ=environ),
|
minute_path(name, timestr, environ=environ),
|
||||||
),
|
),
|
||||||
|
five_minute_bar_reader=BcolzFiveMinuteBarReader(
|
||||||
|
five_minute_path(name, timestr, environ=environ),
|
||||||
|
),
|
||||||
daily_bar_reader=BcolzDailyBarReader(
|
daily_bar_reader=BcolzDailyBarReader(
|
||||||
daily_path(name, timestr, environ=environ),
|
daily_path(name, timestr, environ=environ),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
# 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 sys
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -25,8 +23,6 @@ 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
|
|
||||||
|
|
||||||
class PoloniexBundle(BaseCryptoPricingBundle):
|
class PoloniexBundle(BaseCryptoPricingBundle):
|
||||||
@lazyval
|
@lazyval
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -40,13 +36,14 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
def frequencies(self):
|
def frequencies(self):
|
||||||
return set((
|
return set((
|
||||||
'daily',
|
'daily',
|
||||||
'minute',
|
#'5-minute',
|
||||||
))
|
))
|
||||||
|
|
||||||
@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://www.dropbox.com/s/9naqffawnq8o4r2/'
|
||||||
|
'poloniex-bundle.tar?dl=1'
|
||||||
)
|
)
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
@@ -79,14 +76,12 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
start_date = sym_data.index[0]
|
start_date = sym_data.index[0]
|
||||||
end_date = sym_data.index[-1]
|
end_date = sym_data.index[-1]
|
||||||
ac_date = end_date + pd.Timedelta(days=1)
|
ac_date = end_date + pd.Timedelta(days=1)
|
||||||
min_trade_size = 0.00000001
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
sym_md.symbol,
|
sym_md.symbol,
|
||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
ac_date,
|
ac_date,
|
||||||
min_trade_size,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def fetch_raw_symbol_frame(self,
|
def fetch_raw_symbol_frame(self,
|
||||||
@@ -96,29 +91,18 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
frequency):
|
frequency):
|
||||||
|
raw = pd.read_json(
|
||||||
|
self._format_data_url(
|
||||||
|
api_key,
|
||||||
|
symbol,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
frequency,
|
||||||
|
),
|
||||||
|
orient='records',
|
||||||
|
)
|
||||||
|
raw.set_index('date', inplace=True)
|
||||||
|
|
||||||
# TODO: replace this with direct exchange call
|
|
||||||
# The end date and frequency should be used to calculate the number of bars
|
|
||||||
if(frequency == 'minute'):
|
|
||||||
pc = PoloniexCurator()
|
|
||||||
raw = pc.onemin_to_dataframe(symbol, start_date, end_date)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raw = pd.read_json(
|
|
||||||
self._format_data_url(
|
|
||||||
api_key,
|
|
||||||
symbol,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
frequency,
|
|
||||||
),
|
|
||||||
orient='records',
|
|
||||||
)
|
|
||||||
raw.set_index('date', inplace=True)
|
|
||||||
|
|
||||||
# BcolzDailyBarReader introduces a 1/1000 factor in the way pricing is stored
|
|
||||||
# on disk, which we compensate here to get the right pricing amounts
|
|
||||||
# ref: data/us_equity_pricing.py
|
|
||||||
scale = 1
|
scale = 1
|
||||||
raw.loc[:, 'open'] /= scale
|
raw.loc[:, 'open'] /= scale
|
||||||
raw.loc[:, 'high'] /= scale
|
raw.loc[:, 'high'] /= scale
|
||||||
@@ -148,6 +132,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
data_frequency):
|
data_frequency):
|
||||||
period_map = {
|
period_map = {
|
||||||
'daily': 86400,
|
'daily': 86400,
|
||||||
|
# '5-minute': 300,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -166,7 +151,6 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
return self._format_polo_query(query_params)
|
return self._format_polo_query(query_params)
|
||||||
|
|
||||||
def _format_polo_query(self, query_params):
|
def _format_polo_query(self, query_params):
|
||||||
# TODO: got against the exchange object
|
|
||||||
return 'https://poloniex.com/public?{query}'.format(
|
return 'https://poloniex.com/public?{query}'.format(
|
||||||
query=urlencode(query_params),
|
query=urlencode(query_params),
|
||||||
)
|
)
|
||||||
@@ -180,9 +164,4 @@ 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)
|
||||||
'''
|
'''
|
||||||
|
register_bundle(PoloniexBundle)
|
||||||
if 'ingest' in sys.argv and '-c' in sys.argv:
|
|
||||||
register_bundle(PoloniexBundle)
|
|
||||||
else:
|
|
||||||
register_bundle(PoloniexBundle, create_writers=False)
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ from catalyst.utils.cli import maybe_show_progress
|
|||||||
|
|
||||||
from . import core as bundles
|
from . import core as bundles
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
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):
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from catalyst.assets.roll_finder import (
|
|||||||
)
|
)
|
||||||
from catalyst.data.dispatch_bar_reader import (
|
from catalyst.data.dispatch_bar_reader import (
|
||||||
AssetDispatchMinuteBarReader,
|
AssetDispatchMinuteBarReader,
|
||||||
|
AssetDispatchFiveMinuteBarReader,
|
||||||
AssetDispatchSessionBarReader
|
AssetDispatchSessionBarReader
|
||||||
)
|
)
|
||||||
from catalyst.data.resample import (
|
from catalyst.data.resample import (
|
||||||
@@ -68,9 +69,7 @@ from catalyst.errors import (
|
|||||||
HistoryWindowStartsBeforeData,
|
HistoryWindowStartsBeforeData,
|
||||||
)
|
)
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = Logger('DataPortal')
|
||||||
|
|
||||||
log = Logger('DataPortal', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
BASE_FIELDS = frozenset([
|
BASE_FIELDS = frozenset([
|
||||||
"open",
|
"open",
|
||||||
@@ -121,6 +120,10 @@ class DataPortal(object):
|
|||||||
daily data backtests or daily history calls in a minute backetest.
|
daily data backtests or daily history calls in a minute backetest.
|
||||||
If a daily bar reader is not provided but a minute bar reader is,
|
If a daily bar reader is not provided but a minute bar reader is,
|
||||||
the minutes will be rolled up to serve the daily requests.
|
the minutes will be rolled up to serve the daily requests.
|
||||||
|
five_minute_reader : BcolzFiveMinuteBarReader, optional
|
||||||
|
The five minute bar reader for equities. This will be used to service
|
||||||
|
5-minute data backtests or five-minute history calls. This can be used
|
||||||
|
to serve daily calls if no daily bar reader is provided.
|
||||||
minute_reader : BcolzMinuteBarReader, optional
|
minute_reader : BcolzMinuteBarReader, optional
|
||||||
The minute bar reader for equities. This will be used to service
|
The minute bar reader for equities. This will be used to service
|
||||||
minute data backtests or minute history calls. This can be used
|
minute data backtests or minute history calls. This can be used
|
||||||
@@ -147,6 +150,7 @@ class DataPortal(object):
|
|||||||
trading_calendar,
|
trading_calendar,
|
||||||
first_trading_day,
|
first_trading_day,
|
||||||
daily_reader=None,
|
daily_reader=None,
|
||||||
|
five_minute_reader=None,
|
||||||
minute_reader=None,
|
minute_reader=None,
|
||||||
future_daily_reader=None,
|
future_daily_reader=None,
|
||||||
future_minute_reader=None,
|
future_minute_reader=None,
|
||||||
@@ -198,6 +202,7 @@ class DataPortal(object):
|
|||||||
reader.last_available_dt
|
reader.last_available_dt
|
||||||
for reader in [
|
for reader in [
|
||||||
minute_reader,
|
minute_reader,
|
||||||
|
five_minute_reader,
|
||||||
future_minute_reader,
|
future_minute_reader,
|
||||||
]
|
]
|
||||||
if reader is not None
|
if reader is not None
|
||||||
@@ -209,6 +214,8 @@ class DataPortal(object):
|
|||||||
|
|
||||||
aligned_minute_reader = self._ensure_reader_aligned(
|
aligned_minute_reader = self._ensure_reader_aligned(
|
||||||
minute_reader)
|
minute_reader)
|
||||||
|
aligned_five_minute_reader = self._ensure_reader_aligned(
|
||||||
|
five_minute_reader)
|
||||||
aligned_session_reader = self._ensure_reader_aligned(
|
aligned_session_reader = self._ensure_reader_aligned(
|
||||||
daily_reader)
|
daily_reader)
|
||||||
aligned_future_minute_reader = self._ensure_reader_aligned(
|
aligned_future_minute_reader = self._ensure_reader_aligned(
|
||||||
@@ -222,10 +229,13 @@ class DataPortal(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
aligned_minute_readers = {}
|
aligned_minute_readers = {}
|
||||||
|
aligned_five_minute_readers = {}
|
||||||
aligned_session_readers = {}
|
aligned_session_readers = {}
|
||||||
|
|
||||||
if aligned_minute_reader is not None:
|
if aligned_minute_reader is not None:
|
||||||
aligned_minute_readers[Equity] = aligned_minute_reader
|
aligned_minute_readers[Equity] = aligned_minute_reader
|
||||||
|
if aligned_five_minute_reader is not None:
|
||||||
|
aligned_five_minute_readers[Equity] = aligned_five_minute_reader
|
||||||
if aligned_session_reader is not None:
|
if aligned_session_reader is not None:
|
||||||
aligned_session_readers[Equity] = aligned_session_reader
|
aligned_session_readers[Equity] = aligned_session_reader
|
||||||
|
|
||||||
@@ -257,6 +267,13 @@ class DataPortal(object):
|
|||||||
self._last_available_minute,
|
self._last_available_minute,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_dispatch_five_minute_reader = AssetDispatchFiveMinuteBarReader(
|
||||||
|
self.trading_calendar,
|
||||||
|
self.asset_finder,
|
||||||
|
aligned_five_minute_readers,
|
||||||
|
self._last_available_minute,
|
||||||
|
)
|
||||||
|
|
||||||
_dispatch_session_reader = AssetDispatchSessionBarReader(
|
_dispatch_session_reader = AssetDispatchSessionBarReader(
|
||||||
self.trading_calendar,
|
self.trading_calendar,
|
||||||
self.asset_finder,
|
self.asset_finder,
|
||||||
@@ -266,6 +283,7 @@ class DataPortal(object):
|
|||||||
|
|
||||||
self._pricing_readers = {
|
self._pricing_readers = {
|
||||||
'minute': _dispatch_minute_reader,
|
'minute': _dispatch_minute_reader,
|
||||||
|
'5-minute': _dispatch_five_minute_reader,
|
||||||
'daily': _dispatch_session_reader,
|
'daily': _dispatch_session_reader,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,6 +719,17 @@ class DataPortal(object):
|
|||||||
spot_value=result
|
spot_value=result
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_five_minute_spot_value(self, asset, column, dt, ffill=False):
|
||||||
|
return self._get_minutely_spot_value(
|
||||||
|
asset,
|
||||||
|
column,
|
||||||
|
dt,
|
||||||
|
ffill,
|
||||||
|
'5-minute',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_minute_spot_value(self, asset, column, dt, ffill=False):
|
def _get_minute_spot_value(self, asset, column, dt, ffill=False):
|
||||||
return self._get_minutely_spot_value(
|
return self._get_minutely_spot_value(
|
||||||
asset,
|
asset,
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from numpy import (
|
|||||||
full,
|
full,
|
||||||
nan,
|
nan,
|
||||||
int64,
|
int64,
|
||||||
float64,
|
|
||||||
zeros
|
zeros
|
||||||
)
|
)
|
||||||
from six import iteritems, with_metaclass
|
from six import iteritems, with_metaclass
|
||||||
@@ -71,9 +70,7 @@ class AssetDispatchBarReader(with_metaclass(ABCMeta)):
|
|||||||
return self._dt_window_size(start_dt, end_dt), num_sids
|
return self._dt_window_size(start_dt, end_dt), num_sids
|
||||||
|
|
||||||
def _make_raw_array_out(self, field, shape):
|
def _make_raw_array_out(self, field, shape):
|
||||||
if field == 'volume':
|
if field != 'volume' and field != 'sid':
|
||||||
out = zeros(shape, dtype=float64)
|
|
||||||
elif field != 'sid':
|
|
||||||
out = full(shape, nan)
|
out = full(shape, nan)
|
||||||
else:
|
else:
|
||||||
out = zeros(shape, dtype=int64)
|
out = zeros(shape, dtype=int64)
|
||||||
@@ -138,6 +135,12 @@ 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 AssetDispatchFiveMinuteBarReader(AssetDispatchBarReader):
|
||||||
|
|
||||||
|
def _dt_window_size(self, start_dt, end_dt):
|
||||||
|
return len(self.trading_calendar.five_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):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,7 @@ from catalyst.utils.numpy_utils import float64_dtype
|
|||||||
from catalyst.utils.pandas_utils import find_in_sorted_index
|
from catalyst.utils.pandas_utils import find_in_sorted_index
|
||||||
|
|
||||||
# Default number of decimal places used for rounding asset prices.
|
# Default number of decimal places used for rounding asset prices.
|
||||||
DEFAULT_ASSET_PRICE_DECIMALS = 9
|
DEFAULT_ASSET_PRICE_DECIMALS = 3
|
||||||
|
|
||||||
|
|
||||||
class HistoryCompatibleUSEquityAdjustmentReader(object):
|
class HistoryCompatibleUSEquityAdjustmentReader(object):
|
||||||
|
|||||||
+66
-130
@@ -12,38 +12,41 @@
|
|||||||
# 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
|
||||||
|
|
||||||
import logbook
|
import logbook
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytz
|
import numpy as np
|
||||||
from pandas_datareader.data import DataReader
|
from pandas_datareader.data import DataReader
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
import pytz
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
from six.moves.urllib_error import HTTPError
|
from six.moves.urllib_error import HTTPError
|
||||||
|
|
||||||
from catalyst.utils.calendars import get_calendar
|
|
||||||
from . import treasuries, treasuries_can
|
|
||||||
from .benchmarks import get_benchmark_returns
|
from .benchmarks import get_benchmark_returns
|
||||||
from ..utils.deprecate import deprecated
|
from . import treasuries, treasuries_can
|
||||||
from ..utils.paths import (
|
from ..utils.paths import (
|
||||||
cache_root,
|
cache_root,
|
||||||
data_root,
|
data_root,
|
||||||
)
|
)
|
||||||
|
from ..utils.deprecate import deprecated
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.data.bundles.poloniex import PoloniexBundle
|
||||||
|
from catalyst.utils.calendars import get_calendar
|
||||||
|
|
||||||
logger = logbook.Logger('Loader', level=LOG_LEVEL)
|
|
||||||
|
logger = logbook.Logger('Loader')
|
||||||
|
|
||||||
# Mapping from index symbol to appropriate bond data
|
# Mapping from index symbol to appropriate bond data
|
||||||
INDEX_MAPPING = {
|
INDEX_MAPPING = {
|
||||||
'SPY':
|
'SPY':
|
||||||
(treasuries, 'treasury_curves.csv', 'www.federalreserve.gov'),
|
(treasuries, 'treasury_curves.csv', 'www.federalreserve.gov'),
|
||||||
'^GSPTSE':
|
'^GSPTSE':
|
||||||
(treasuries_can, 'treasury_curves_can.csv', 'bankofcanada.ca'),
|
(treasuries_can, 'treasury_curves_can.csv', 'bankofcanada.ca'),
|
||||||
'^FTSE': # use US treasuries until UK bonds implemented
|
'^FTSE': # use US treasuries until UK bonds implemented
|
||||||
(treasuries, 'treasury_curves.csv', 'www.federalreserve.gov'),
|
(treasuries, 'treasury_curves.csv', 'www.federalreserve.gov'),
|
||||||
}
|
}
|
||||||
|
|
||||||
ONE_HOUR = pd.Timedelta(hours=1)
|
ONE_HOUR = pd.Timedelta(hours=1)
|
||||||
@@ -91,27 +94,19 @@ def has_data_for_dates(series_or_df, first_date, last_date):
|
|||||||
if not isinstance(dts, pd.DatetimeIndex):
|
if not isinstance(dts, pd.DatetimeIndex):
|
||||||
raise TypeError("Expected a DatetimeIndex, but got %s." % type(dts))
|
raise TypeError("Expected a DatetimeIndex, but got %s." % type(dts))
|
||||||
first, last = dts[[0, -1]].tz_localize(None)
|
first, last = dts[[0, -1]].tz_localize(None)
|
||||||
return (first <= first_date.tz_localize(None)) and (
|
return (first <= first_date.tz_localize(None)) and (last >= last_date.tz_localize(None))
|
||||||
last >= last_date.tz_localize(None))
|
|
||||||
|
|
||||||
|
def load_crypto_market_data(trading_day=None,
|
||||||
def load_crypto_market_data(trading_day=None, trading_days=None,
|
trading_days=None,
|
||||||
bm_symbol=None, bundle=None, bundle_data=None,
|
bm_symbol='USDT_BTC',
|
||||||
environ=None, exchange=None, start_dt=None,
|
environ=None):
|
||||||
end_dt=None):
|
|
||||||
if trading_day is None:
|
if trading_day is None:
|
||||||
trading_day = get_calendar('OPEN').trading_day
|
trading_day = get_calendar('OPEN').trading_day
|
||||||
|
if trading_days is None:
|
||||||
|
trading_days = get_calendar('OPEN').all_sessions
|
||||||
|
|
||||||
# TODO: consider making configurable
|
first_date = trading_days[0]
|
||||||
bm_symbol = 'btc_usdt'
|
now = pd.Timestamp.utcnow()
|
||||||
# if trading_days is None:
|
|
||||||
# trading_days = get_calendar('OPEN').schedule
|
|
||||||
|
|
||||||
# if start_dt is None:
|
|
||||||
start_dt = get_calendar('OPEN').first_trading_session
|
|
||||||
|
|
||||||
if end_dt is None:
|
|
||||||
end_dt = pd.Timestamp.utcnow()
|
|
||||||
|
|
||||||
# We expect to have benchmark and treasury data that's current up until
|
# We expect to have benchmark and treasury data that's current up until
|
||||||
# **two** full trading days prior to the most recently completed trading
|
# **two** full trading days prior to the most recently completed trading
|
||||||
@@ -127,56 +122,30 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
|
|
||||||
# We'll attempt to download new data if the latest entry in our cache is
|
# We'll attempt to download new data if the latest entry in our cache is
|
||||||
# before this date.
|
# before this date.
|
||||||
'''
|
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
||||||
if(bundle_data):
|
|
||||||
# If we are using the bundle to retrieve the cryptobenchmark, find 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)
|
|
||||||
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')
|
|
||||||
else:
|
|
||||||
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
|
||||||
'''
|
|
||||||
last_date = trading_days[trading_days.get_loc(end_dt, method='ffill') - 1]
|
|
||||||
|
|
||||||
if exchange is None:
|
|
||||||
# This is exceptional, since placing the import at the module scope
|
|
||||||
# breaks things and it's only needed here
|
|
||||||
from catalyst.exchange.factory import get_exchange
|
|
||||||
exchange = get_exchange(
|
|
||||||
exchange_name='poloniex', base_currency='usdt'
|
|
||||||
)
|
|
||||||
|
|
||||||
benchmark_asset = exchange.get_asset(bm_symbol)
|
|
||||||
|
|
||||||
# exchange.get_history_window() already ensures that we have the right data
|
|
||||||
# for the right dates
|
|
||||||
br = exchange.get_history_window_with_bundle(
|
|
||||||
assets=[benchmark_asset],
|
|
||||||
end_dt=last_date,
|
|
||||||
bar_count=pd.Timedelta(last_date - start_dt).days,
|
|
||||||
frequency='1d',
|
|
||||||
field='close',
|
|
||||||
data_frequency='daily',
|
|
||||||
force_auto_ingest=True)
|
|
||||||
br.columns = ['close']
|
|
||||||
br = br.pct_change(1).iloc[1:]
|
|
||||||
br.loc[start_dt] = 0
|
|
||||||
br = br.sort_index()
|
|
||||||
|
|
||||||
|
br = ensure_crypto_benchmark_data(
|
||||||
|
bm_symbol,
|
||||||
|
first_date,
|
||||||
|
last_date,
|
||||||
|
now,
|
||||||
|
# We need the trading_day to figure out the close prior to the first
|
||||||
|
# date so that we can compute returns for the first date.
|
||||||
|
trading_day,
|
||||||
|
environ,
|
||||||
|
)
|
||||||
# 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 years
|
||||||
# and is independent of crypto data
|
# and is independent of crypto data
|
||||||
first_date_treasury = pd.Timestamp('1990-01-02', tz='UTC')
|
first_date_treasury = pd.Timestamp('1990-01-01', tz='UTC')
|
||||||
tc = ensure_treasury_data(
|
tc = ensure_treasury_data(
|
||||||
bm_symbol,
|
bm_symbol,
|
||||||
first_date_treasury,
|
first_date_treasury,
|
||||||
last_date,
|
last_date,
|
||||||
end_dt,
|
now,
|
||||||
environ,
|
environ,
|
||||||
)
|
)
|
||||||
benchmark_returns = br[br.index.slice_indexer(start_dt, last_date)]
|
benchmark_returns = br[br.index.slice_indexer(first_date, last_date)]
|
||||||
treasury_curves = tc[
|
treasury_curves = tc[tc.index.slice_indexer(first_date_treasury, last_date)]
|
||||||
tc.index.slice_indexer(first_date_treasury, last_date)]
|
|
||||||
return benchmark_returns, treasury_curves
|
return benchmark_returns, treasury_curves
|
||||||
|
|
||||||
|
|
||||||
@@ -271,16 +240,15 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
last_date,
|
last_date,
|
||||||
now,
|
now,
|
||||||
trading_day,
|
trading_day,
|
||||||
bundle,
|
|
||||||
bundle_data,
|
|
||||||
environ=None):
|
environ=None):
|
||||||
|
|
||||||
filename = get_benchmark_filename(symbol)
|
filename = get_benchmark_filename(symbol)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
('Loading benchmark data for {symbol!r} '
|
('Loading benchmark data for {symbol!r} '
|
||||||
'from {first_date} to {last_date}'),
|
'from {first_date} to {last_date}'),
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
first_date=first_date,
|
first_date=first_date - trading_day,
|
||||||
last_date=last_date
|
last_date=last_date
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -293,65 +261,34 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
environ,
|
environ,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if data is not None:
|
if data is not None:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# If no cached data was found or it was missing any dates then download the
|
# If no cached data was found or it was missing any dates then download the
|
||||||
# necessary data.
|
# 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
|
||||||
|
)
|
||||||
|
|
||||||
if (bundle == 'poloniex'):
|
# Load benchmark symbol from Poloniex API
|
||||||
'''
|
try:
|
||||||
If we're using the Poloniex bundle, we'll get the benchmark from the bundle
|
bundle = PoloniexBundle()
|
||||||
instead of downloading it from Poloniex every time we need it.
|
bench_raw = bundle._fetch_symbol_frame(
|
||||||
Poloniex has a captcha for API queries originating from outside the US that
|
None,
|
||||||
prevents users abroad from getting Catalyst to work
|
symbol,
|
||||||
'''
|
get_calendar(bundle.calendar_name),
|
||||||
logger.info(
|
first_date - trading_day,
|
||||||
(
|
last_date,
|
||||||
'Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'),
|
'daily',
|
||||||
symbol=symbol, first_date=first_date, last_date=last_date)
|
)
|
||||||
|
except (OSError, IOError, HTTPError):
|
||||||
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,
|
logger.exception('Failed to fetch new crypto benchmark returns')
|
||||||
as_of_date=None)
|
raise
|
||||||
fields = ['day', 'close']
|
|
||||||
raw = bundle_data.daily_bar_reader.load_raw_arrays(
|
|
||||||
columns=fields,
|
|
||||||
start_date=first_date - trading_day,
|
|
||||||
end_date=last_date,
|
|
||||||
assets=[asset, ])
|
|
||||||
bench_raw = pd.concat([pd.DataFrame(raw[0], columns=['date']),
|
|
||||||
pd.DataFrame(raw[1], columns=['close'])],
|
|
||||||
axis=1)
|
|
||||||
bench_raw['date'] = pd.to_datetime(bench_raw['date'], unit='s')
|
|
||||||
bench_raw.set_index('date', inplace=True)
|
|
||||||
bench_raw.sort_index(inplace=True)
|
|
||||||
bench_raw = bench_raw[
|
|
||||||
pd.to_datetime(first_date - trading_day):pd.to_datetime(
|
|
||||||
last_date)]
|
|
||||||
|
|
||||||
else:
|
|
||||||
# This is how it used to be: downloading the benchmark everytime.
|
|
||||||
# Leaving this code here to be repurposed in the future for other bundles.
|
|
||||||
logger.info(
|
|
||||||
(
|
|
||||||
'Downloading benchmark data for {symbol!r} from {first_date} to {last_date}'),
|
|
||||||
symbol=symbol, first_date=first_date, last_date=last_date)
|
|
||||||
|
|
||||||
raise DeprecationWarning('poloniex bundle deprecated')
|
|
||||||
# Load benchmark symbol from Poloniex API
|
|
||||||
# try:
|
|
||||||
# bundle = PoloniexBundle()
|
|
||||||
# bench_raw = bundle._fetch_symbol_frame(
|
|
||||||
# None,
|
|
||||||
# symbol,
|
|
||||||
# get_calendar(bundle.calendar_name),
|
|
||||||
# first_date - trading_day,
|
|
||||||
# last_date,
|
|
||||||
# 'daily',
|
|
||||||
# )
|
|
||||||
# except (OSError, IOError, HTTPError):
|
|
||||||
# logger.exception('Failed to fetch new crypto benchmark returns')
|
|
||||||
# raise
|
|
||||||
|
|
||||||
# select close column and compute percent change between days
|
# select close column and compute percent change between days
|
||||||
daily_close = bench_raw[['close']]
|
daily_close = bench_raw[['close']]
|
||||||
@@ -410,7 +347,7 @@ def ensure_benchmark_data(symbol, first_date, last_date, now, trading_day,
|
|||||||
# necessary data.
|
# necessary data.
|
||||||
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,
|
symbol=symbol,
|
||||||
first_date=first_date - trading_day,
|
first_date=first_date - trading_day,
|
||||||
last_date=last_date
|
last_date=last_date
|
||||||
@@ -471,7 +408,7 @@ def ensure_benchmark_data(symbol, first_date, last_date, now, trading_day,
|
|||||||
# necessary data.
|
# necessary data.
|
||||||
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,
|
symbol=symbol,
|
||||||
first_date=first_date - trading_day,
|
first_date=first_date - trading_day,
|
||||||
last_date=last_date
|
last_date=last_date
|
||||||
@@ -555,8 +492,7 @@ def _load_cached_data(filename, first_date, last_date, now, resource_name,
|
|||||||
data = pd.DataFrame.from_csv(path)
|
data = pd.DataFrame.from_csv(path)
|
||||||
if data.empty:
|
if data.empty:
|
||||||
raise ValueError("File is empty.")
|
raise ValueError("File is empty.")
|
||||||
data.index = pd.to_datetime(data.index, infer_datetime_format=True,
|
data.index = pd.to_datetime(data.index, infer_datetime_format=True, errors='coerce' ).tz_localize('UTC')
|
||||||
errors='coerce').tz_localize('UTC')
|
|
||||||
if has_data_for_dates(data, first_date, last_date):
|
if has_data_for_dates(data, first_date, last_date):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -582,7 +518,7 @@ def _load_cached_data(filename, first_date, last_date, now, resource_name,
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Cache at {path} does not have data from {start} to {end}.",
|
"Cache at {path} does not have data from {start} to {end}.\n",
|
||||||
start=first_date,
|
start=first_date,
|
||||||
end=last_date,
|
end=last_date,
|
||||||
path=path,
|
path=path,
|
||||||
|
|||||||
@@ -39,21 +39,20 @@ from catalyst.data._minute_bar_internal import (
|
|||||||
from catalyst.gens.sim_engine import NANOS_IN_MINUTE
|
from catalyst.gens.sim_engine import NANOS_IN_MINUTE
|
||||||
|
|
||||||
from catalyst.data.bar_reader import BarReader, NoDataOnDate
|
from catalyst.data.bar_reader import BarReader, NoDataOnDate
|
||||||
from catalyst.data.us_equity_pricing import check_uint64_safe
|
from catalyst.data.us_equity_pricing import check_uint32_safe
|
||||||
from catalyst.utils.calendars import get_calendar
|
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
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
|
|
||||||
logger = logbook.Logger('MinuteBars', level=LOG_LEVEL)
|
logger = logbook.Logger('MinuteBars')
|
||||||
|
|
||||||
US_EQUITIES_MINUTES_PER_DAY = 390
|
US_EQUITIES_MINUTES_PER_DAY = 390
|
||||||
FUTURES_MINUTES_PER_DAY = 1440
|
FUTURES_MINUTES_PER_DAY = 1440
|
||||||
|
|
||||||
DEFAULT_EXPECTEDLEN = US_EQUITIES_MINUTES_PER_DAY * 252 * 15
|
DEFAULT_EXPECTEDLEN = US_EQUITIES_MINUTES_PER_DAY * 252 * 15
|
||||||
|
|
||||||
OHLC_RATIO = 100000000
|
OHLC_RATIO = 1000
|
||||||
|
|
||||||
|
|
||||||
class BcolzMinuteOverlappingData(Exception):
|
class BcolzMinuteOverlappingData(Exception):
|
||||||
@@ -115,15 +114,15 @@ def _sid_subdir_path(sid):
|
|||||||
|
|
||||||
|
|
||||||
def convert_cols(cols, scale_factor, sid, invalid_data_behavior):
|
def convert_cols(cols, scale_factor, sid, invalid_data_behavior):
|
||||||
"""Adapt OHLCV columns into uint64 columns.
|
"""Adapt OHLCV columns into uint32 columns.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
cols : dict
|
cols : dict
|
||||||
A dict mapping each column name (open, high, low, close, volume)
|
A dict mapping each column name (open, high, low, close, volume)
|
||||||
to a float column to convert to uint64.
|
to a float column to convert to uint32.
|
||||||
scale_factor : int
|
scale_factor : int
|
||||||
Factor to use to scale float values before converting to uint64.
|
Factor to use to scale float values before converting to uint32.
|
||||||
sid : int
|
sid : int
|
||||||
Sid of the relevant asset, for logging.
|
Sid of the relevant asset, for logging.
|
||||||
invalid_data_behavior : str
|
invalid_data_behavior : str
|
||||||
@@ -136,7 +135,6 @@ def convert_cols(cols, scale_factor, sid, invalid_data_behavior):
|
|||||||
scaled_highs = np.nan_to_num(cols['high']) * scale_factor
|
scaled_highs = np.nan_to_num(cols['high']) * scale_factor
|
||||||
scaled_lows = np.nan_to_num(cols['low']) * scale_factor
|
scaled_lows = np.nan_to_num(cols['low']) * scale_factor
|
||||||
scaled_closes = np.nan_to_num(cols['close']) * scale_factor
|
scaled_closes = np.nan_to_num(cols['close']) * scale_factor
|
||||||
scaled_volumes = np.nan_to_num(cols['volume']) * scale_factor
|
|
||||||
|
|
||||||
exclude_mask = np.zeros_like(scaled_opens, dtype=bool)
|
exclude_mask = np.zeros_like(scaled_opens, dtype=bool)
|
||||||
|
|
||||||
@@ -145,12 +143,11 @@ def convert_cols(cols, scale_factor, sid, invalid_data_behavior):
|
|||||||
('high', scaled_highs),
|
('high', scaled_highs),
|
||||||
('low', scaled_lows),
|
('low', scaled_lows),
|
||||||
('close', scaled_closes),
|
('close', scaled_closes),
|
||||||
('volume', scaled_volumes),
|
|
||||||
]:
|
]:
|
||||||
max_val = scaled_col.max()
|
max_val = scaled_col.max()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
check_uint64_safe(max_val, col_name)
|
check_uint32_safe(max_val, col_name)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if invalid_data_behavior == 'raise':
|
if invalid_data_behavior == 'raise':
|
||||||
raise
|
raise
|
||||||
@@ -158,20 +155,20 @@ def convert_cols(cols, scale_factor, sid, invalid_data_behavior):
|
|||||||
if invalid_data_behavior == 'warn':
|
if invalid_data_behavior == 'warn':
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'Values for sid={}, col={} contain some too large for '
|
'Values for sid={}, col={} contain some too large for '
|
||||||
'uint64 (max={}), filtering them out',
|
'uint32 (max={}), filtering them out',
|
||||||
sid, col_name, max_val,
|
sid, col_name, max_val,
|
||||||
)
|
)
|
||||||
|
|
||||||
# We want to exclude all rows that have an unsafe value in
|
# We want to exclude all rows that have an unsafe value in
|
||||||
# this column.
|
# this column.
|
||||||
exclude_mask &= (scaled_col >= np.iinfo(np.uint64).max)
|
exclude_mask &= (scaled_col >= np.iinfo(np.uint32).max)
|
||||||
|
|
||||||
# Convert all cols to uint32.
|
# Convert all cols to uint32.
|
||||||
opens = scaled_opens.astype(np.uint64)
|
opens = scaled_opens.astype(np.uint32)
|
||||||
highs = scaled_highs.astype(np.uint64)
|
highs = scaled_highs.astype(np.uint32)
|
||||||
lows = scaled_lows.astype(np.uint64)
|
lows = scaled_lows.astype(np.uint32)
|
||||||
closes = scaled_closes.astype(np.uint64)
|
closes = scaled_closes.astype(np.uint32)
|
||||||
volumes = scaled_volumes.astype(np.uint64)
|
volumes = cols['volume'].astype(np.uint32)
|
||||||
|
|
||||||
# Exclude rows with unsafe values by setting to zero.
|
# Exclude rows with unsafe values by setting to zero.
|
||||||
opens[exclude_mask] = 0
|
opens[exclude_mask] = 0
|
||||||
@@ -263,14 +260,14 @@ class BcolzMinuteBarMetadata(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
default_ohlc_ratio,
|
default_ohlc_ratio,
|
||||||
ohlc_ratios_per_sid,
|
ohlc_ratios_per_sid,
|
||||||
calendar,
|
calendar,
|
||||||
start_session,
|
start_session,
|
||||||
end_session,
|
end_session,
|
||||||
minutes_per_day,
|
minutes_per_day,
|
||||||
version=FORMAT_VERSION,
|
version=FORMAT_VERSION,
|
||||||
):
|
):
|
||||||
self.calendar = calendar
|
self.calendar = calendar
|
||||||
self.start_session = start_session
|
self.start_session = start_session
|
||||||
@@ -291,7 +288,7 @@ class BcolzMinuteBarMetadata(object):
|
|||||||
ohlc_ratio : int
|
ohlc_ratio : int
|
||||||
The default ratio by which to multiply the pricing data to
|
The default ratio by which to multiply the pricing data to
|
||||||
convert the floats from floats to an integer to fit within
|
convert the floats from floats to an integer to fit within
|
||||||
the np.uint64. If ohlc_ratios_per_sid is None or does not
|
the np.uint32. If ohlc_ratios_per_sid is None or does not
|
||||||
contain a mapping for a given sid, this ratio is used.
|
contain a mapping for a given sid, this ratio is used.
|
||||||
ohlc_ratios_per_sid : dict
|
ohlc_ratios_per_sid : dict
|
||||||
A dict mapping each sid in the output to the factor by
|
A dict mapping each sid in the output to the factor by
|
||||||
@@ -343,10 +340,10 @@ class BcolzMinuteBarMetadata(object):
|
|||||||
'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': (
|
||||||
market_closes.values.astype('datetime64[m]').
|
market_closes.values.astype('datetime64[m]').
|
||||||
astype(np.int64).tolist()),
|
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)
|
||||||
@@ -375,13 +372,13 @@ class BcolzMinuteBarWriter(object):
|
|||||||
The last trading session in the data set.
|
The last trading session in the data set.
|
||||||
default_ohlc_ratio : int, optional
|
default_ohlc_ratio : int, optional
|
||||||
The default ratio by which to multiply the pricing data to
|
The default ratio by which to multiply the pricing data to
|
||||||
convert from floats to integers that fit within np.uint64. If
|
convert from floats to integers that fit within np.uint32. If
|
||||||
ohlc_ratios_per_sid is None or does not contain a mapping for a
|
ohlc_ratios_per_sid is None or does not contain a mapping for a
|
||||||
given sid, this ratio is used. Default is OHLC_RATIO (10^8).
|
given sid, this ratio is used. Default is OHLC_RATIO (1000).
|
||||||
ohlc_ratios_per_sid : dict, optional
|
ohlc_ratios_per_sid : dict, optional
|
||||||
A dict mapping each sid in the output to the ratio by which to
|
A dict mapping each sid in the output to the ratio by which to
|
||||||
multiply the pricing data to convert the floats from floats to
|
multiply the pricing data to convert the floats from floats to
|
||||||
an integer to fit within the np.uint64.
|
an integer to fit within the np.uint32.
|
||||||
expectedlen : int, optional
|
expectedlen : int, optional
|
||||||
The expected length of the dataset, used when creating the initial
|
The expected length of the dataset, used when creating the initial
|
||||||
bcolz ctable.
|
bcolz ctable.
|
||||||
@@ -404,9 +401,11 @@ class BcolzMinuteBarWriter(object):
|
|||||||
Each individual asset's data is stored as a bcolz table with a column for
|
Each individual asset's data is stored as a bcolz table with a column for
|
||||||
each pricing field: (open, high, low, close, volume)
|
each pricing field: (open, high, low, close, volume)
|
||||||
|
|
||||||
The open, high, low, close and volume columns are integers which are 10^8 times
|
The open, high, low, and close columns are integers which are 1000 times
|
||||||
the quoted price, so that the data can represented and stored as an
|
the quoted price, so that the data can represented and stored as an
|
||||||
np.uint64, supporting market prices quoted up to the 1/10^8-th place.
|
np.uint32, supporting market prices quoted up to the thousands place.
|
||||||
|
|
||||||
|
volume is a np.uint32 with no mutation of the tens place.
|
||||||
|
|
||||||
The 'index' for each individual asset are a repeating period of minutes of
|
The 'index' for each individual asset are a repeating period of minutes of
|
||||||
length `minutes_per_day` starting from each market open.
|
length `minutes_per_day` starting from each market open.
|
||||||
@@ -574,7 +573,7 @@ class BcolzMinuteBarWriter(object):
|
|||||||
if not os.path.exists(sid_containing_dirname):
|
if not os.path.exists(sid_containing_dirname):
|
||||||
# Other sids may have already created the containing directory.
|
# Other sids may have already created the containing directory.
|
||||||
os.makedirs(sid_containing_dirname)
|
os.makedirs(sid_containing_dirname)
|
||||||
initial_array = np.empty(0, np.uint64)
|
initial_array = np.empty(0, np.uint32)
|
||||||
table = ctable(
|
table = ctable(
|
||||||
rootdir=path,
|
rootdir=path,
|
||||||
columns=[
|
columns=[
|
||||||
@@ -611,7 +610,7 @@ class BcolzMinuteBarWriter(object):
|
|||||||
minute_offset = len(table) % self._minutes_per_day
|
minute_offset = len(table) % self._minutes_per_day
|
||||||
num_to_prepend = numdays * self._minutes_per_day - minute_offset
|
num_to_prepend = numdays * self._minutes_per_day - minute_offset
|
||||||
|
|
||||||
prepend_array = np.zeros(num_to_prepend, np.uint64)
|
prepend_array = np.zeros(num_to_prepend, np.uint32)
|
||||||
# Fill all OHLCV with zeros.
|
# Fill all OHLCV with zeros.
|
||||||
table.append([prepend_array] * 5)
|
table.append([prepend_array] * 5)
|
||||||
table.flush()
|
table.flush()
|
||||||
@@ -816,11 +815,11 @@ class BcolzMinuteBarWriter(object):
|
|||||||
|
|
||||||
minutes_count = all_minutes_in_window.size
|
minutes_count = all_minutes_in_window.size
|
||||||
|
|
||||||
open_col = np.zeros(minutes_count, dtype=np.uint64)
|
open_col = np.zeros(minutes_count, dtype=np.uint32)
|
||||||
high_col = np.zeros(minutes_count, dtype=np.uint64)
|
high_col = np.zeros(minutes_count, dtype=np.uint32)
|
||||||
low_col = np.zeros(minutes_count, dtype=np.uint64)
|
low_col = np.zeros(minutes_count, dtype=np.uint32)
|
||||||
close_col = np.zeros(minutes_count, dtype=np.uint64)
|
close_col = np.zeros(minutes_count, dtype=np.uint32)
|
||||||
vol_col = np.zeros(minutes_count, dtype=np.uint64)
|
vol_col = np.zeros(minutes_count, dtype=np.uint32)
|
||||||
|
|
||||||
dt_ixs = np.searchsorted(all_minutes_in_window.values,
|
dt_ixs = np.searchsorted(all_minutes_in_window.values,
|
||||||
dts.astype('datetime64[ns]'))
|
dts.astype('datetime64[ns]'))
|
||||||
@@ -915,10 +914,10 @@ class BcolzMinuteBarReader(MinuteBarReader):
|
|||||||
)
|
)
|
||||||
self._schedule = self.calendar.schedule[slicer]
|
self._schedule = self.calendar.schedule[slicer]
|
||||||
self._market_opens = self._schedule.market_open
|
self._market_opens = self._schedule.market_open
|
||||||
self._market_open_values = self._market_opens.values. \
|
self._market_open_values = self._market_opens.values.\
|
||||||
astype('datetime64[m]').astype(np.int64)
|
astype('datetime64[m]').astype(np.int64)
|
||||||
self._market_closes = self._schedule.market_close
|
self._market_closes = self._schedule.market_close
|
||||||
self._market_close_values = self._market_closes.values. \
|
self._market_close_values = self._market_closes.values.\
|
||||||
astype('datetime64[m]').astype(np.int64)
|
astype('datetime64[m]').astype(np.int64)
|
||||||
|
|
||||||
self._default_ohlc_inverse = 1.0 / metadata.default_ohlc_ratio
|
self._default_ohlc_inverse = 1.0 / metadata.default_ohlc_ratio
|
||||||
@@ -1126,8 +1125,8 @@ class BcolzMinuteBarReader(MinuteBarReader):
|
|||||||
else:
|
else:
|
||||||
return np.nan
|
return np.nan
|
||||||
|
|
||||||
# if field != 'volume':
|
if field != 'volume':
|
||||||
value *= self._ohlc_ratio_inverse_for_sid(sid)
|
value *= self._ohlc_ratio_inverse_for_sid(sid)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def get_last_traded_dt(self, asset, dt):
|
def get_last_traded_dt(self, asset, dt):
|
||||||
@@ -1249,7 +1248,7 @@ class BcolzMinuteBarReader(MinuteBarReader):
|
|||||||
if field != 'volume':
|
if field != 'volume':
|
||||||
out = np.full(shape, np.nan)
|
out = np.full(shape, np.nan)
|
||||||
else:
|
else:
|
||||||
out = np.zeros(shape, dtype=np.float64)
|
out = np.zeros(shape, dtype=np.uint32)
|
||||||
|
|
||||||
for i, sid in enumerate(sids):
|
for i, sid in enumerate(sids):
|
||||||
carray = self._open_minute_file(field, sid)
|
carray = self._open_minute_file(field, sid)
|
||||||
@@ -1257,17 +1256,17 @@ class BcolzMinuteBarReader(MinuteBarReader):
|
|||||||
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 - start_idx + 1]
|
excl_start - start_idx:excl_stop - start_idx + 1]
|
||||||
values = np.delete(values, excl_slice)
|
values = np.delete(values, excl_slice)
|
||||||
|
|
||||||
where = values != 0
|
where = values != 0
|
||||||
# first slice down to len(where) because we might not have
|
# first slice down to len(where) because we might not have
|
||||||
# written data for all the minutes requested
|
# written data for all the minutes requested
|
||||||
# if field != 'volume':
|
if field != 'volume':
|
||||||
out[:len(where), i][where] = (
|
out[:len(where), i][where] = (
|
||||||
values[where] * self._ohlc_ratio_inverse_for_sid(sid))
|
values[where] * self._ohlc_ratio_inverse_for_sid(sid))
|
||||||
# else:
|
else:
|
||||||
# out[:len(where), i][where] = values[where]
|
out[:len(where), i][where] = values[where]
|
||||||
|
|
||||||
results.append(out)
|
results.append(out)
|
||||||
return results
|
return results
|
||||||
@@ -1320,9 +1319,9 @@ 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):
|
||||||
@@ -1354,7 +1353,6 @@ class H5MinuteBarUpdateReader(MinuteBarUpdateReader):
|
|||||||
path : str
|
path : str
|
||||||
The path of the HDF5 file from which to source data.
|
The path of the HDF5 file from which to source data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
self._panel = pd.read_hdf(path)
|
self._panel = pd.read_hdf(path)
|
||||||
|
|
||||||
|
|||||||
@@ -156,10 +156,7 @@ class DailyHistoryAggregator(object):
|
|||||||
cache = self._caches[field] = (session, market_open, {})
|
cache = self._caches[field] = (session, market_open, {})
|
||||||
|
|
||||||
_, market_open, entries = cache
|
_, market_open, entries = cache
|
||||||
try:
|
market_open = market_open.tz_localize('UTC')
|
||||||
market_open = market_open.tz_localize('UTC')
|
|
||||||
except TypeError:
|
|
||||||
market_open = market_open.tz_convert('UTC')
|
|
||||||
if dt != market_open:
|
if dt != market_open:
|
||||||
prev_dt = dt_value - self._one_min
|
prev_dt = dt_value - self._one_min
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -11,9 +11,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.
|
||||||
|
|
||||||
from __future__ import division # Python2 req to have division of ints yield float
|
|
||||||
|
|
||||||
from errno import ENOENT
|
from errno import ENOENT
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from os import remove
|
from os import remove
|
||||||
@@ -83,9 +80,8 @@ 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
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
|
|
||||||
logger = logbook.Logger('UsEquityPricing', level=LOG_LEVEL)
|
logger = logbook.Logger('UsEquityPricing')
|
||||||
|
|
||||||
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'])
|
||||||
@@ -120,8 +116,6 @@ 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
|
|
||||||
|
|
||||||
|
|
||||||
def check_uint32_safe(value, colname):
|
def check_uint32_safe(value, colname):
|
||||||
if value >= UINT32_MAX:
|
if value >= UINT32_MAX:
|
||||||
@@ -439,11 +433,11 @@ 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)] * 1000000).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.astype('uint64')
|
||||||
return ctable.fromdataframe(processed)
|
return ctable.fromdataframe(processed)
|
||||||
|
|
||||||
|
|
||||||
@@ -496,8 +490,9 @@ class BcolzDailyBarReader(SessionBarReader):
|
|||||||
|
|
||||||
The data in these columns is interpreted as follows:
|
The data in these columns is interpreted as follows:
|
||||||
|
|
||||||
- Price columns ('open', 'high', 'low', 'close') and Volume are interpreted
|
- Price columns ('open', 'high', 'low', 'close') are interpreted as 1000 *
|
||||||
as 10^9 * as-traded dollar value.
|
as-traded dollar value.
|
||||||
|
- Volume is interpreted as as-traded volume.
|
||||||
- Day is interpreted as seconds since midnight UTC, Jan 1, 1970.
|
- Day is interpreted as seconds since midnight UTC, Jan 1, 1970.
|
||||||
- Id is the asset id of the row.
|
- Id is the asset id of the row.
|
||||||
|
|
||||||
@@ -524,6 +519,7 @@ class BcolzDailyBarReader(SessionBarReader):
|
|||||||
# Need to test keeping the entire array in memory for the course of a
|
# Need to test keeping the entire array in memory for the course of a
|
||||||
# process first.
|
# process first.
|
||||||
self._spot_cols = {}
|
self._spot_cols = {}
|
||||||
|
self.PRICE_ADJUSTMENT_FACTOR = 0.001
|
||||||
self._read_all_threshold = read_all_threshold
|
self._read_all_threshold = read_all_threshold
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
@@ -763,10 +759,13 @@ class BcolzDailyBarReader(SessionBarReader):
|
|||||||
"""
|
"""
|
||||||
ix = self.sid_day_index(sid, dt)
|
ix = self.sid_day_index(sid, dt)
|
||||||
price = self._spot_col(field)[ix]
|
price = self._spot_col(field)[ix]
|
||||||
if field != 'volume' and price == 0:
|
if field != 'volume':
|
||||||
return nan
|
if price == 0:
|
||||||
|
return nan
|
||||||
|
else:
|
||||||
|
return price * 0.001
|
||||||
else:
|
else:
|
||||||
return price / PRICE_ADJUSTMENT_FACTOR
|
return price
|
||||||
|
|
||||||
|
|
||||||
class PanelBarReader(SessionBarReader):
|
class PanelBarReader(SessionBarReader):
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
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.
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
record,
|
|
||||||
order,
|
|
||||||
symbol,
|
|
||||||
get_open_orders
|
|
||||||
)
|
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
|
||||||
|
|
||||||
algo_namespace = 'arbitrage_eth_btc'
|
|
||||||
log = Logger(algo_namespace)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
log.info('initializing arbitrage algorithm')
|
|
||||||
|
|
||||||
# The context contains a new "exchanges" attribute which is a dictionary
|
|
||||||
# of exchange objects by exchange name. This allow easy access to the
|
|
||||||
# exchanges.
|
|
||||||
context.buying_exchange = context.exchanges['poloniex']
|
|
||||||
context.selling_exchange = context.exchanges['bitfinex']
|
|
||||||
|
|
||||||
context.trading_pair_symbol = 'eth_btc'
|
|
||||||
context.trading_pairs = dict()
|
|
||||||
|
|
||||||
# Note the second parameter of the symbol() method
|
|
||||||
# Passing the exchange name here returns a TradingPair object including
|
|
||||||
# the exchange information. This allow all other operations using
|
|
||||||
# the TradingPair to target the correct exchange.
|
|
||||||
context.trading_pairs[context.buying_exchange] = \
|
|
||||||
symbol('eth_btc', context.buying_exchange.name)
|
|
||||||
|
|
||||||
context.trading_pairs[context.selling_exchange] = \
|
|
||||||
symbol(context.trading_pair_symbol, context.selling_exchange.name)
|
|
||||||
|
|
||||||
context.entry_points = [
|
|
||||||
dict(gap=0.03, amount=0.05),
|
|
||||||
dict(gap=0.04, amount=0.1),
|
|
||||||
dict(gap=0.05, amount=0.5),
|
|
||||||
]
|
|
||||||
context.exit_points = [
|
|
||||||
dict(gap=-0.02, amount=0.5),
|
|
||||||
]
|
|
||||||
|
|
||||||
context.SLIPPAGE_ALLOWED = 0.02
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def place_orders(context, amount, buying_price, selling_price, action):
|
|
||||||
"""
|
|
||||||
This method will always place two orders of the same amount to keep
|
|
||||||
the currency position the same as it moves between the two exchanges.
|
|
||||||
|
|
||||||
:param context: TradingAlgorithm
|
|
||||||
:param amount: float
|
|
||||||
The trading pair amount to trade on both exchanges.
|
|
||||||
:param buying_price: float
|
|
||||||
The current trading pair price on the buying exchange.
|
|
||||||
:param selling_price: float
|
|
||||||
The current trading pair price on the selling exchange.
|
|
||||||
:param action: string
|
|
||||||
"enter": buys on the buying exchange and sells on the selling exchange
|
|
||||||
"exit": buys on the selling exchange and sells on the buying exchange
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if action == 'enter':
|
|
||||||
enter_exchange = context.buying_exchange
|
|
||||||
entry_price = buying_price
|
|
||||||
|
|
||||||
exit_exchange = context.selling_exchange
|
|
||||||
exit_price = selling_price
|
|
||||||
|
|
||||||
elif action == 'exit':
|
|
||||||
enter_exchange = context.selling_exchange
|
|
||||||
entry_price = selling_price
|
|
||||||
|
|
||||||
exit_exchange = context.buying_exchange
|
|
||||||
exit_price = buying_price
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError('invalid order action')
|
|
||||||
|
|
||||||
quote_currency = enter_exchange.quote_currency
|
|
||||||
quote_currency_amount = enter_exchange.portfolio.cash
|
|
||||||
|
|
||||||
exit_balances = exit_exchange.get_balances()
|
|
||||||
exit_currency = context.trading_pairs[
|
|
||||||
context.selling_exchange].quote_currency
|
|
||||||
|
|
||||||
if exit_currency in exit_balances:
|
|
||||||
quote_currency_amount = exit_balances[exit_currency]
|
|
||||||
else:
|
|
||||||
log.warn(
|
|
||||||
'the selling exchange {exchange_name} does not hold '
|
|
||||||
'currency {currency}'.format(
|
|
||||||
exchange_name=exit_exchange.name,
|
|
||||||
currency=exit_currency
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if quote_currency_amount < (amount * entry_price):
|
|
||||||
adj_amount = quote_currency_amount / entry_price
|
|
||||||
log.warn(
|
|
||||||
'not enough {quote_currency} ({quote_currency_amount}) to buy '
|
|
||||||
'{amount}, adjusting the amount to {adj_amount}'.format(
|
|
||||||
quote_currency=quote_currency,
|
|
||||||
quote_currency_amount=quote_currency_amount,
|
|
||||||
amount=amount,
|
|
||||||
adj_amount=adj_amount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
amount = adj_amount
|
|
||||||
|
|
||||||
elif quote_currency_amount < amount:
|
|
||||||
log.warn(
|
|
||||||
'not enough {currency} ({currency_amount}) to sell '
|
|
||||||
'{amount}, aborting'.format(
|
|
||||||
currency=exit_currency,
|
|
||||||
currency_amount=quote_currency_amount,
|
|
||||||
amount=amount
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
adj_buy_price = entry_price * (1 + context.SLIPPAGE_ALLOWED)
|
|
||||||
log.info(
|
|
||||||
'buying {amount} {trading_pair} on {exchange_name} with price '
|
|
||||||
'limit {limit_price}'.format(
|
|
||||||
amount=amount,
|
|
||||||
trading_pair=context.trading_pair_symbol,
|
|
||||||
exchange_name=enter_exchange.name,
|
|
||||||
limit_price=adj_buy_price
|
|
||||||
)
|
|
||||||
)
|
|
||||||
order(
|
|
||||||
asset=context.trading_pairs[enter_exchange],
|
|
||||||
amount=amount,
|
|
||||||
limit_price=adj_buy_price
|
|
||||||
)
|
|
||||||
|
|
||||||
adj_sell_price = exit_price * (1 - context.SLIPPAGE_ALLOWED)
|
|
||||||
log.info(
|
|
||||||
'selling {amount} {trading_pair} on {exchange_name} with price '
|
|
||||||
'limit {limit_price}'.format(
|
|
||||||
amount=-amount,
|
|
||||||
trading_pair=context.trading_pair_symbol,
|
|
||||||
exchange_name=exit_exchange.name,
|
|
||||||
limit_price=adj_sell_price
|
|
||||||
)
|
|
||||||
)
|
|
||||||
order(
|
|
||||||
asset=context.trading_pairs[exit_exchange],
|
|
||||||
amount=-amount,
|
|
||||||
limit_price=adj_sell_price
|
|
||||||
)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
log.info('handling bar {}'.format(data.current_dt))
|
|
||||||
|
|
||||||
buying_price = data.current(
|
|
||||||
context.trading_pairs[context.buying_exchange], 'price')
|
|
||||||
|
|
||||||
log.info('price on buying exchange {exchange}: {price}'.format(
|
|
||||||
exchange=context.buying_exchange.name.upper(),
|
|
||||||
price=buying_price,
|
|
||||||
))
|
|
||||||
|
|
||||||
selling_price = data.current(
|
|
||||||
context.trading_pairs[context.selling_exchange], 'price')
|
|
||||||
|
|
||||||
log.info('price on selling exchange {exchange}: {price}'.format(
|
|
||||||
exchange=context.selling_exchange.name.upper(),
|
|
||||||
price=selling_price,
|
|
||||||
))
|
|
||||||
|
|
||||||
# If for example,
|
|
||||||
# selling price = 50
|
|
||||||
# buying price = 25
|
|
||||||
# expected gap = 1
|
|
||||||
|
|
||||||
# If follows that,
|
|
||||||
# selling price - buying price / buying price
|
|
||||||
# 50 - 25 / 25 = 1
|
|
||||||
gap = (selling_price - buying_price) / buying_price
|
|
||||||
log.info(
|
|
||||||
'the price gap: {gap} ({gap_percent}%)'.format(
|
|
||||||
gap=gap,
|
|
||||||
gap_percent=gap * 100
|
|
||||||
)
|
|
||||||
)
|
|
||||||
record(buying_price=buying_price, selling_price=selling_price, gap=gap)
|
|
||||||
|
|
||||||
# Waiting for orders to close before initiating new ones
|
|
||||||
for exchange in context.trading_pairs:
|
|
||||||
asset = context.trading_pairs[exchange]
|
|
||||||
|
|
||||||
orders = get_open_orders(asset)
|
|
||||||
if orders:
|
|
||||||
log.info(
|
|
||||||
'found {order_count} open orders on {exchange_name} '
|
|
||||||
'skipping bar until all open orders execute'.format(
|
|
||||||
order_count=len(orders),
|
|
||||||
exchange_name=exchange.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Consider the least ambitious entry point first
|
|
||||||
# Override of wider gap is found
|
|
||||||
entry_points = sorted(
|
|
||||||
context.entry_points,
|
|
||||||
key=lambda point: point['gap'],
|
|
||||||
)
|
|
||||||
|
|
||||||
buy_amount = None
|
|
||||||
for entry_point in entry_points:
|
|
||||||
if gap > entry_point['gap']:
|
|
||||||
buy_amount = entry_point['amount']
|
|
||||||
|
|
||||||
if buy_amount:
|
|
||||||
log.info('found buy trigger for amount: {}'.format(buy_amount))
|
|
||||||
place_orders(
|
|
||||||
context=context,
|
|
||||||
amount=buy_amount,
|
|
||||||
buying_price=buying_price,
|
|
||||||
selling_price=selling_price,
|
|
||||||
action='enter'
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Consider the narrowest exit gap first
|
|
||||||
# Override of wider gap is found
|
|
||||||
exit_points = sorted(
|
|
||||||
context.exit_points,
|
|
||||||
key=lambda point: point['gap'],
|
|
||||||
reverse=True
|
|
||||||
)
|
|
||||||
|
|
||||||
sell_amount = None
|
|
||||||
for exit_point in exit_points:
|
|
||||||
if gap < exit_point['gap']:
|
|
||||||
sell_amount = exit_point['amount']
|
|
||||||
|
|
||||||
if sell_amount:
|
|
||||||
log.info('found sell trigger for amount: {}'.format(sell_amount))
|
|
||||||
place_orders(
|
|
||||||
context=context,
|
|
||||||
amount=sell_amount,
|
|
||||||
buying_price=buying_price,
|
|
||||||
selling_price=selling_price,
|
|
||||||
action='exit'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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='poloniex,bitfinex',
|
|
||||||
live=True,
|
|
||||||
algo_namespace=algo_namespace,
|
|
||||||
quote_currency='btc',
|
|
||||||
live_graph=False
|
|
||||||
)
|
|
||||||
@@ -14,28 +14,36 @@
|
|||||||
# 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 (
|
||||||
from catalyst.api import (order_target_value, symbol, record,
|
order_target_value,
|
||||||
cancel_order, get_open_orders, )
|
symbol,
|
||||||
|
record,
|
||||||
|
cancel_order,
|
||||||
|
get_open_orders,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.ASSET_NAME = 'btc_usd'
|
context.ASSET_NAME = 'USDT_BTC'
|
||||||
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
|
||||||
|
|
||||||
|
print 'i:', context.i
|
||||||
|
|
||||||
starting_cash = context.portfolio.starting_cash
|
starting_cash = context.portfolio.starting_cash
|
||||||
target_hodl_value = context.TARGET_HODL_RATIO * starting_cash
|
target_hodl_value = context.TARGET_HODL_RATIO * starting_cash
|
||||||
reserve_value = context.RESERVE_RATIO * starting_cash
|
reserve_value = context.RESERVE_RATIO * starting_cash
|
||||||
@@ -51,57 +59,53 @@ 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.current(context.asset, 'price')
|
price = data[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,
|
stop_price=price*0.9,
|
||||||
)
|
)
|
||||||
|
|
||||||
record(
|
record(
|
||||||
price=price,
|
price=price,
|
||||||
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(511)
|
||||||
results[['portfolio_value']].plot(ax=ax1)
|
results[['portfolio_value']].plot(ax=ax1)
|
||||||
ax1.set_ylabel('Portfolio\nValue\n(USD)')
|
ax1.set_ylabel('Portfolio Value (USD)')
|
||||||
|
|
||||||
ax2 = plt.subplot(612, sharex=ax1)
|
ax2 = plt.subplot(512, sharex=ax1)
|
||||||
ax2.set_ylabel('{asset}\n(USD)'.format(asset=context.ASSET_NAME))
|
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
|
||||||
results[['price']].plot(ax=ax2)
|
(context.TICK_SIZE * 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.scatter(
|
ax2.plot(
|
||||||
buys.index.to_pydatetime(),
|
buys.index,
|
||||||
results.price[buys.index],
|
context.TICK_SIZE * results.price[buys.index],
|
||||||
marker='^',
|
'^',
|
||||||
s=100,
|
markersize=10,
|
||||||
c='g',
|
color='g',
|
||||||
label=''
|
|
||||||
)
|
)
|
||||||
|
|
||||||
ax3 = plt.subplot(613, sharex=ax1)
|
ax3 = plt.subplot(513, sharex=ax1)
|
||||||
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
|
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
|
||||||
ax3.set_ylabel('Leverage ')
|
ax3.set_ylabel('Leverage ')
|
||||||
|
|
||||||
ax4 = plt.subplot(614, sharex=ax1)
|
ax4 = plt.subplot(514, sharex=ax1)
|
||||||
results[['starting_cash', 'cash']].plot(ax=ax4)
|
results[['starting_cash', 'cash']].plot(ax=ax4)
|
||||||
ax4.set_ylabel('Cash (USD)')
|
ax4.set_ylabel('Cash (USD)')
|
||||||
|
|
||||||
@@ -115,35 +119,16 @@ def analyze(context=None, results=None):
|
|||||||
'benchmark_period_return',
|
'benchmark_period_return',
|
||||||
]]
|
]]
|
||||||
|
|
||||||
ax5 = plt.subplot(615, sharex=ax1)
|
ax5 = plt.subplot(515, sharex=ax1)
|
||||||
results[[
|
results[[
|
||||||
'treasury',
|
'treasury',
|
||||||
'algorithm',
|
'algorithm',
|
||||||
'benchmark',
|
'benchmark',
|
||||||
]].plot(ax=ax5)
|
]].plot(ax=ax5)
|
||||||
ax5.set_ylabel('Percent\nChange')
|
ax5.set_ylabel('Percent Change')
|
||||||
|
|
||||||
ax6 = plt.subplot(616, sharex=ax1)
|
|
||||||
results[['volume']].plot(ax=ax6)
|
|
||||||
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='bitfinex',
|
|
||||||
algo_namespace='buy_and_hodl',
|
|
||||||
base_currency='usd',
|
|
||||||
start=pd.to_datetime('2015-03-01', utc=True),
|
|
||||||
end=pd.to_datetime('2017-10-31', utc=True),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
from datetime import datetime
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
from catalyst.api import (
|
||||||
|
order_target_value,
|
||||||
|
symbol,
|
||||||
|
record,
|
||||||
|
cancel_order,
|
||||||
|
get_open_orders,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.ASSET_NAME = 'USDT_BTC'
|
||||||
|
context.TARGET_HODL_RATIO = 0.8
|
||||||
|
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.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
|
context.i = 0
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
context.i += 1
|
||||||
|
|
||||||
|
print 'i:', context.i
|
||||||
|
|
||||||
|
starting_cash = context.portfolio.starting_cash
|
||||||
|
target_hodl_value = context.TARGET_HODL_RATIO * starting_cash
|
||||||
|
reserve_value = context.RESERVE_RATIO * starting_cash
|
||||||
|
|
||||||
|
# Cancel any outstanding orders
|
||||||
|
orders = get_open_orders(context.asset) or []
|
||||||
|
for order in orders:
|
||||||
|
cancel_order(order)
|
||||||
|
|
||||||
|
# Stop buying after passing the reserve threshold
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
if cash <= reserve_value:
|
||||||
|
context.is_buying = False
|
||||||
|
|
||||||
|
# Retrieve current asset price from pricing data
|
||||||
|
price = data[context.asset].price
|
||||||
|
|
||||||
|
# Check if still buying and could (approximately) afford another purchase
|
||||||
|
if context.is_buying and cash > price:
|
||||||
|
# Place order to make position in asset equal to target_hodl_value
|
||||||
|
order_target_value(
|
||||||
|
context.asset,
|
||||||
|
target_hodl_value,
|
||||||
|
limit_price=price * 1.1,
|
||||||
|
stop_price=price * 0.9,
|
||||||
|
)
|
||||||
|
|
||||||
|
record(
|
||||||
|
price=price,
|
||||||
|
cash=cash,
|
||||||
|
starting_cash=context.portfolio.starting_cash,
|
||||||
|
leverage=context.account.leverage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
start = datetime(2015, 3, 1, 0, 0, 0, 0, pytz.utc)
|
||||||
|
end = datetime(2017, 6, 28, 0, 0, 0, 0, pytz.utc)
|
||||||
|
run_algorithm(
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
capital_base=100000,
|
||||||
|
bundle='poloniex'
|
||||||
|
)
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
'''
|
|
||||||
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.api import order, record, symbol
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
context.asset = symbol('btc_usd')
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
order(context.asset, 1)
|
|
||||||
record(btc = data.current(context.asset, 'price'))
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
'''
|
|
||||||
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
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
order,
|
|
||||||
order_target_percent,
|
|
||||||
symbol,
|
|
||||||
record,
|
|
||||||
get_open_orders,
|
|
||||||
)
|
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
|
||||||
|
|
||||||
algo_namespace = 'buy_low_sell_high_xrp'
|
|
||||||
log = Logger(algo_namespace)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
log.info('initializing algo')
|
|
||||||
context.ASSET_NAME = 'XRP_USDT'
|
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
|
||||||
|
|
||||||
context.TARGET_POSITIONS = 5000
|
|
||||||
context.PROFIT_TARGET = 0.1
|
|
||||||
context.SLIPPAGE_ALLOWED = 0.05
|
|
||||||
|
|
||||||
context.retry_check_open_orders = 10
|
|
||||||
context.retry_update_portfolio = 10
|
|
||||||
context.retry_order = 5
|
|
||||||
|
|
||||||
context.swallow_errors = True
|
|
||||||
|
|
||||||
context.errors = []
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_data(context, data):
|
|
||||||
prices = data.history(
|
|
||||||
context.asset,
|
|
||||||
fields='price',
|
|
||||||
bar_count=20,
|
|
||||||
frequency='15m'
|
|
||||||
)
|
|
||||||
|
|
||||||
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 = 50
|
|
||||||
elif rsi <= 40:
|
|
||||||
buy_increment = 20
|
|
||||||
elif rsi <= 70:
|
|
||||||
buy_increment = 5
|
|
||||||
else:
|
|
||||||
buy_increment = None
|
|
||||||
|
|
||||||
cash = context.portfolio.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(
|
|
||||||
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
|
|
||||||
+16
-29
@@ -1,7 +1,6 @@
|
|||||||
import talib
|
import talib
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
from catalyst.api import (
|
from catalyst.api import (
|
||||||
order,
|
order,
|
||||||
order_target_percent,
|
order_target_percent,
|
||||||
@@ -9,19 +8,18 @@ from catalyst.api import (
|
|||||||
record,
|
record,
|
||||||
get_open_orders,
|
get_open_orders,
|
||||||
)
|
)
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
algo_namespace = 'buy_the_dip_live'
|
algo_namespace = 'buy_the_dip_live'
|
||||||
log = Logger('buy low sell high')
|
log = Logger(algo_namespace)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
log.info('initializing algo')
|
log.info('initializing algo')
|
||||||
context.ASSET_NAME = 'btc_usdt'
|
context.ASSET_NAME = 'XRP_USD'
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
context.TARGET_POSITIONS = 30
|
context.TARGET_POSITIONS = 5000
|
||||||
context.PROFIT_TARGET = 0.1
|
context.PROFIT_TARGET = 0.1
|
||||||
context.SLIPPAGE_ALLOWED = 0.02
|
context.SLIPPAGE_ALLOWED = 0.02
|
||||||
|
|
||||||
@@ -34,31 +32,31 @@ def initialize(context):
|
|||||||
|
|
||||||
|
|
||||||
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='1d'
|
frequency='15m'
|
||||||
)
|
)
|
||||||
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 = 1
|
buy_increment = 50
|
||||||
elif rsi <= 40:
|
elif rsi <= 40:
|
||||||
buy_increment = 0.5
|
buy_increment = 20
|
||||||
elif rsi <= 70:
|
elif rsi <= 70:
|
||||||
buy_increment = 0.2
|
buy_increment = 5
|
||||||
else:
|
else:
|
||||||
buy_increment = 0.1
|
buy_increment = None
|
||||||
|
|
||||||
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,
|
||||||
@@ -142,27 +140,16 @@ 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 full stats:\n{}'.format(stats.head()))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
run_algorithm(
|
||||||
capital_base=100000,
|
|
||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
analyze=analyze,
|
analyze=analyze,
|
||||||
exchange_name='poloniex',
|
exchange_name='bitfinex',
|
||||||
start=pd.to_datetime('2017-5-01', utc=True),
|
live=True,
|
||||||
end=pd.to_datetime('2017-10-16', utc=True),
|
algo_namespace=algo_namespace,
|
||||||
base_currency='usdt',
|
base_currency='usd'
|
||||||
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,153 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from logbook import Logger
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.api import (order, record, symbol, order_target_percent,
|
|
||||||
get_open_orders)
|
|
||||||
from catalyst.exchange.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_mavg = data.history(context.asset, 'price',
|
|
||||||
bar_count=short_window, frequency="1m").mean()
|
|
||||||
long_mavg = data.history(context.asset, 'price',
|
|
||||||
bar_count=long_window, frequency="1m").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 = get_open_orders(context.asset)
|
|
||||||
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
|
|
||||||
base_currency = context.exchanges.values()[0].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,279 +0,0 @@
|
|||||||
# 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.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.neo_eth = symbol('neo_eth')
|
|
||||||
context.base_price = None
|
|
||||||
context.current_day = None
|
|
||||||
|
|
||||||
context.RSI_OVERSOLD = 55
|
|
||||||
context.RSI_OVERBOUGHT = 82
|
|
||||||
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 = False
|
|
||||||
context.current_day = today
|
|
||||||
|
|
||||||
# We're computing the volume-weighted-average-price of the security
|
|
||||||
# defined above, in the context.neo_eth 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.neo_eth,
|
|
||||||
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.neo_eth, 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(
|
|
||||||
price=price,
|
|
||||||
volume=current['volume'],
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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.neo_eth)
|
|
||||||
if len(orders) > 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Exit if we cannot trade
|
|
||||||
if not data.can_trade(context.neo_eth):
|
|
||||||
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.neo_eth].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.neo_eth, 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.neo_eth, 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 = 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.neo_eth.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
|
|
||||||
MODE = 'live'
|
|
||||||
|
|
||||||
if MODE == 'backtest':
|
|
||||||
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))
|
|
||||||
|
|
||||||
elif MODE == 'live':
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=0.1,
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='bittrex',
|
|
||||||
live=True,
|
|
||||||
algo_namespace=NAMESPACE,
|
|
||||||
base_currency='eth',
|
|
||||||
live_graph=False
|
|
||||||
)
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
'''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
|
|
||||||
from scipy.optimize import minimize
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from catalyst.api import record, symbol, 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 xrange(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')
|
|
||||||
|
|
||||||
|
|
||||||
# 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, )
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
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.exchange.stats_utils import crossover, crossunder
|
|
||||||
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=17,
|
|
||||||
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 = 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
|
|
||||||
|
|
||||||
|
|
||||||
# run_algorithm(
|
|
||||||
# initialize=initialize,
|
|
||||||
# handle_data=handle_data,
|
|
||||||
# analyze=analyze,
|
|
||||||
# exchange_name='bittrex',
|
|
||||||
# live=True,
|
|
||||||
# algo_namespace=algo_namespace,
|
|
||||||
# base_currency='btc',
|
|
||||||
# live_graph=False
|
|
||||||
# )
|
|
||||||
|
|
||||||
# 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),
|
|
||||||
)
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import talib
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.api import symbol, record
|
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats, \
|
|
||||||
extract_transactions
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
print('initializing')
|
|
||||||
context.asset = symbol('neo_eth')
|
|
||||||
context.base_price = None
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
print('handling bar: {}'.format(data.current_dt))
|
|
||||||
|
|
||||||
price = data.current(context.asset, 'close')
|
|
||||||
print('got price {price}'.format(price=price))
|
|
||||||
|
|
||||||
prices = data.history(
|
|
||||||
context.asset,
|
|
||||||
fields='price',
|
|
||||||
bar_count=20,
|
|
||||||
frequency='15T'
|
|
||||||
)
|
|
||||||
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
|
||||||
print('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
|
|
||||||
print('the stats: {}'.format(get_pretty_stats(perf)))
|
|
||||||
|
|
||||||
# The base currency of the algo exchange
|
|
||||||
base_currency = 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
|
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=250,
|
|
||||||
start=pd.to_datetime('2017-11-1 0:00', utc=True),
|
|
||||||
end=pd.to_datetime('2017-11-10 23:59', utc=True),
|
|
||||||
data_frequency='daily',
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='bitfinex',
|
|
||||||
algo_namespace='simple_loop',
|
|
||||||
base_currency='usd'
|
|
||||||
)
|
|
||||||
# run_algorithm(
|
|
||||||
# initialize=initialize,
|
|
||||||
# handle_data=handle_data,
|
|
||||||
# analyze=None,
|
|
||||||
# exchange_name='binance',
|
|
||||||
# live=True,
|
|
||||||
# algo_namespace='simple_loop',
|
|
||||||
# base_currency='eth',
|
|
||||||
# live_graph=False,
|
|
||||||
# )
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
"""
|
|
||||||
Requires Catalyst version 0.3.0 or above
|
|
||||||
Tested on Catalyst version 0.3.3
|
|
||||||
|
|
||||||
These example aims to provide and easy way for users to learn how to collect data from the different exchanges.
|
|
||||||
You simply need to specify the exchange and the market that you want to focus on.
|
|
||||||
You will all see how to create a universe and filter it base on the exchange and the market you desire.
|
|
||||||
|
|
||||||
The example prints out the closing price of all the pairs for a given market-exchange every 30 minutes.
|
|
||||||
The example also contains the ohlcv minute data for the past seven days which could be used to create indicators
|
|
||||||
Use this as the backbone to create your own trading strategies.
|
|
||||||
|
|
||||||
Variables lookback date and date are used to ensure data for a coin existed on the lookback period specified.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from datetime import timedelta
|
|
||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols
|
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
symbols,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
context.i = -1 # counts the minutes
|
|
||||||
context.exchange = context.exchanges.values()[0].name.lower() # exchange name
|
|
||||||
context.base_currency = context.exchanges.values()[0].base_currency.lower() # market base currency
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
context.i += 1
|
|
||||||
lookback_days = 7 # 7 days
|
|
||||||
|
|
||||||
# current date formatted into a string
|
|
||||||
today = data.current_dt
|
|
||||||
date, time = today.strftime('%Y-%m-%d %H:%M:%S').split(' ')
|
|
||||||
lookback_date = today - timedelta(days=lookback_days) # subtract the amount of days specified in lookback
|
|
||||||
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[0] # get only the date as a string
|
|
||||||
|
|
||||||
# update universe everyday
|
|
||||||
new_day = 60 * 24 # assuming data_frequency='minute'
|
|
||||||
if not context.i % new_day:
|
|
||||||
context.universe = universe(context, lookback_date, date)
|
|
||||||
|
|
||||||
# get data every 30 minutes
|
|
||||||
minutes = 30
|
|
||||||
one_day_in_minutes = 1440 # 1440 assumes data_frequency='minute'
|
|
||||||
lookback = one_day_in_minutes / minutes * lookback_days # get N lookback_days of history data
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 30 minute interval ohlcv data (the standard data required for candlestick or indicators/signals)
|
|
||||||
# 30T means 30 minutes re-sampling of one minute data. change to your desire time interval.
|
|
||||||
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 equivalent to current price
|
|
||||||
# displays the minute price for each pair every 30 minutes
|
|
||||||
print(today, pair, opened[-1], high[-1], low[-1], close[-1], 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):
|
|
||||||
json_symbols = get_exchange_symbols(context.exchange) # get all the pairs for the exchange
|
|
||||||
universe_df = pd.DataFrame.from_dict(json_symbols).transpose().astype(str) # convert into a dataframe
|
|
||||||
universe_df['base_currency'] = universe_df.apply(lambda row: row.symbol.split('_')[1],
|
|
||||||
axis=1)
|
|
||||||
universe_df['market_currency'] = universe_df.apply(lambda row: row.symbol.split('_')[0],
|
|
||||||
axis=1)
|
|
||||||
|
|
||||||
# Filter all the exchange pairs to only the ones for a give base currency
|
|
||||||
universe_df = universe_df[universe_df['base_currency'] == context.base_currency]
|
|
||||||
|
|
||||||
# Filter all the pairs to ensure that pair existed in the current date range
|
|
||||||
universe_df = universe_df[universe_df.start_date < lookback_date]
|
|
||||||
universe_df = universe_df[universe_df.end_daily >= current_date]
|
|
||||||
context.coins = symbols(*universe_df.symbol) # convert all the pairs to symbols
|
|
||||||
|
|
||||||
# print(universe_df.symbol.tolist())
|
|
||||||
return universe_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, not always in dollars unless usd
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='bitfinex',
|
|
||||||
data_frequency='minute',
|
|
||||||
base_currency='btc',
|
|
||||||
live=False,
|
|
||||||
live_graph=False,
|
|
||||||
algo_namespace='simple_universe')
|
|
||||||
|
|
||||||
"""
|
|
||||||
Run in Terminal (inside catalyst environment):
|
|
||||||
python simple_universe.py
|
|
||||||
"""
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
# 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.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),
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
#
|
||||||
|
# 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 os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import pickle
|
||||||
|
from datetime import timedelta
|
||||||
|
from time import sleep
|
||||||
|
from os import listdir
|
||||||
|
from os.path import isfile, join
|
||||||
|
|
||||||
|
import logbook
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
import catalyst.protocol as zp
|
||||||
|
from catalyst.algorithm import TradingAlgorithm
|
||||||
|
from catalyst.data.minute_bars import BcolzMinuteBarWriter, \
|
||||||
|
BcolzMinuteBarReader
|
||||||
|
from catalyst.errors import OrderInBeforeTradingStart
|
||||||
|
from catalyst.exchange.exchange_clock import ExchangeClock
|
||||||
|
from catalyst.exchange.exchange_errors import (
|
||||||
|
ExchangeRequestError,
|
||||||
|
ExchangePortfolioDataError,
|
||||||
|
ExchangeTransactionError
|
||||||
|
)
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \
|
||||||
|
save_algo_object, get_algo_object, get_algo_folder
|
||||||
|
from catalyst.finance.performance.period import calc_period_stats
|
||||||
|
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
||||||
|
from catalyst.utils.api_support import (
|
||||||
|
api_method,
|
||||||
|
disallowed_in_before_trading_start)
|
||||||
|
from catalyst.utils.input_validation import error_keywords
|
||||||
|
|
||||||
|
log = logbook.Logger("ExchangeTradingAlgorithm")
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeAlgorithmExecutor(AlgorithmSimulator):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(self.__class__, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeTradingAlgorithm(TradingAlgorithm):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.exchange = kwargs.pop('exchange', None)
|
||||||
|
self.algo_namespace = kwargs.pop('algo_namespace', None)
|
||||||
|
self.orders = {}
|
||||||
|
self.is_running = True
|
||||||
|
|
||||||
|
self.retry_check_open_orders = 5
|
||||||
|
self.retry_update_portfolio = 5
|
||||||
|
self.retry_get_open_orders = 5
|
||||||
|
self.retry_order = 2
|
||||||
|
self.retry_delay = 5
|
||||||
|
|
||||||
|
super(self.__class__, self).__init__(*args, **kwargs)
|
||||||
|
self._create_minute_writer()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
|
|
||||||
|
log.info('exchange trading algorithm successfully initialized')
|
||||||
|
|
||||||
|
def _create_minute_writer(self):
|
||||||
|
root = get_exchange_minute_writer_root(self.exchange.name)
|
||||||
|
filename = os.path.join(root, 'metadata.json')
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
writer = BcolzMinuteBarWriter.open(
|
||||||
|
root, self.sim_params.end_session)
|
||||||
|
else:
|
||||||
|
writer = BcolzMinuteBarWriter(
|
||||||
|
rootdir=root,
|
||||||
|
calendar=self.trading_calendar,
|
||||||
|
minutes_per_day=1440,
|
||||||
|
start_session=self.sim_params.start_session,
|
||||||
|
end_session=self.sim_params.end_session,
|
||||||
|
write_metadata=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.exchange.minute_writer = writer
|
||||||
|
self.exchange.minute_reader = BcolzMinuteBarReader(root)
|
||||||
|
|
||||||
|
def signal_handler(self, signal, frame):
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
log.info('You pressed Ctrl+C!')
|
||||||
|
|
||||||
|
stats = None
|
||||||
|
try:
|
||||||
|
algo_folder = get_algo_folder(self.algo_namespace)
|
||||||
|
folder = join(algo_folder, 'daily_perf')
|
||||||
|
files = [f for f in listdir(folder) if isfile(join(folder, f))]
|
||||||
|
|
||||||
|
daily_perf_list = []
|
||||||
|
for item in files:
|
||||||
|
filename = join(folder, item)
|
||||||
|
with open(filename, 'rb') as handle:
|
||||||
|
daily_perf_list.append(pickle.load(handle))
|
||||||
|
|
||||||
|
stats = pd.DataFrame(daily_perf_list)
|
||||||
|
stats.set_index('period_close', drop=True, inplace=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('Unable to compute daily stats: {}'.format(e))
|
||||||
|
|
||||||
|
self.analyze(stats)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def _create_clock(self):
|
||||||
|
|
||||||
|
# The calendar's execution times are the minutes over which we actually
|
||||||
|
# want to run the clock. Typically the execution times simply adhere to
|
||||||
|
# the market open and close times. In the case of the futures calendar,
|
||||||
|
# for example, we only want to simulate over a subset of the full 24
|
||||||
|
# hour calendar, so the execution times dictate a market open time of
|
||||||
|
# 6:31am US/Eastern and a close of 5:00pm US/Eastern.
|
||||||
|
|
||||||
|
# In our case, we are trading around the clock, so the market close
|
||||||
|
# corresponds to the last minute of the day.
|
||||||
|
|
||||||
|
# This method is taken from TradingAlgorithm.
|
||||||
|
# The clock has been replaced to use RealtimeClock
|
||||||
|
# TODO: should we apply a time skew? not sure to understand the utility.
|
||||||
|
return ExchangeClock(
|
||||||
|
self.sim_params.sessions,
|
||||||
|
time_skew=self.exchange.time_skew
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_generator(self, sim_params):
|
||||||
|
if self.perf_tracker is None:
|
||||||
|
self.perf_tracker = get_algo_object(
|
||||||
|
algo_name=self.algo_namespace,
|
||||||
|
key='perf_tracker'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call the simulation trading algorithm for side-effects:
|
||||||
|
# it creates the perf tracker
|
||||||
|
TradingAlgorithm._create_generator(self, sim_params)
|
||||||
|
self.trading_client = ExchangeAlgorithmExecutor(
|
||||||
|
self,
|
||||||
|
sim_params,
|
||||||
|
self.data_portal,
|
||||||
|
self._create_clock(),
|
||||||
|
self._create_benchmark_source(),
|
||||||
|
self.restrictions,
|
||||||
|
universe_func=self._calculate_universe
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.trading_client.transform()
|
||||||
|
|
||||||
|
def updated_portfolio(self):
|
||||||
|
"""
|
||||||
|
We skip the entire performance tracker business and update the
|
||||||
|
portfolio directly.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.exchange.portfolio
|
||||||
|
|
||||||
|
def updated_account(self):
|
||||||
|
return self.exchange.account
|
||||||
|
|
||||||
|
def _update_portfolio(self, attempt_index=0):
|
||||||
|
try:
|
||||||
|
self.exchange.update_portfolio()
|
||||||
|
|
||||||
|
# Applying the updated last_sales_price to the positions
|
||||||
|
# in the performance tracker. This seems a bit redundant
|
||||||
|
# but it will make sense when we have multiple exchange portfolios
|
||||||
|
# feeding into the same performance tracker.
|
||||||
|
tracker = self.perf_tracker.todays_performance.position_tracker
|
||||||
|
for asset in self.exchange.portfolio.positions:
|
||||||
|
position = self.exchange.portfolio.positions[asset]
|
||||||
|
tracker.update_position(
|
||||||
|
asset=asset,
|
||||||
|
last_sale_date=position.last_sale_date,
|
||||||
|
last_sale_price=position.last_sale_price
|
||||||
|
)
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
|
'update portfolio attempt {}: {}'.format(attempt_index, e)
|
||||||
|
)
|
||||||
|
if attempt_index < self.retry_update_portfolio:
|
||||||
|
sleep(self.retry_delay)
|
||||||
|
self._update_portfolio(attempt_index + 1)
|
||||||
|
else:
|
||||||
|
raise ExchangePortfolioDataError(
|
||||||
|
data_type='update-portfolio',
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_open_orders(self, attempt_index=0):
|
||||||
|
try:
|
||||||
|
return self.exchange.check_open_orders()
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
|
'check open orders attempt {}: {}'.format(attempt_index, e)
|
||||||
|
)
|
||||||
|
if attempt_index < self.retry_check_open_orders:
|
||||||
|
sleep(self.retry_delay)
|
||||||
|
return self._check_open_orders(attempt_index + 1)
|
||||||
|
else:
|
||||||
|
raise ExchangePortfolioDataError(
|
||||||
|
data_type='order-status',
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
|
def prepare_period_stats(self, start_dt, end_dt):
|
||||||
|
"""
|
||||||
|
Creates a dictionary representing the state of the tracker.
|
||||||
|
|
||||||
|
|
||||||
|
I rewrote this in an attempt to better control the stats.
|
||||||
|
I don't want things to happen magically through complex logic
|
||||||
|
pertaining to backtesting.
|
||||||
|
|
||||||
|
"""
|
||||||
|
tracker = self.perf_tracker
|
||||||
|
period = tracker.todays_performance
|
||||||
|
|
||||||
|
pos_stats = period.position_tracker.stats()
|
||||||
|
period_stats = calc_period_stats(pos_stats, period.ending_cash)
|
||||||
|
|
||||||
|
stats = dict(
|
||||||
|
period_start=tracker.period_start,
|
||||||
|
period_end=tracker.period_end,
|
||||||
|
capital_base=tracker.capital_base,
|
||||||
|
progress=tracker.progress,
|
||||||
|
ending_value=period.ending_value,
|
||||||
|
ending_exposure=period.ending_exposure,
|
||||||
|
capital_used=period.cash_flow,
|
||||||
|
starting_value=period.starting_value,
|
||||||
|
starting_exposure=period.starting_exposure,
|
||||||
|
starting_cash=period.starting_cash,
|
||||||
|
ending_cash=period.ending_cash,
|
||||||
|
portfolio_value=period.ending_cash + period.ending_value,
|
||||||
|
pnl=period.pnl,
|
||||||
|
returns=period.returns,
|
||||||
|
period_open=period.period_open,
|
||||||
|
period_close=period.period_close,
|
||||||
|
gross_leverage=period_stats.gross_leverage,
|
||||||
|
net_leverage=period_stats.net_leverage,
|
||||||
|
short_exposure=pos_stats.short_exposure,
|
||||||
|
long_exposure=pos_stats.long_exposure,
|
||||||
|
short_value=pos_stats.short_value,
|
||||||
|
long_value=pos_stats.long_value,
|
||||||
|
longs_count=pos_stats.longs_count,
|
||||||
|
shorts_count=pos_stats.shorts_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Merging cumulative risk
|
||||||
|
stats.update(tracker.cumulative_risk_metrics.to_dict())
|
||||||
|
|
||||||
|
# Merging latest recorded variables
|
||||||
|
stats.update(self.recorded_vars)
|
||||||
|
|
||||||
|
stats['positions'] = period.position_tracker.get_positions_list()
|
||||||
|
|
||||||
|
# we want the key to be absent, not just empty
|
||||||
|
# Only include transactions for given dt
|
||||||
|
stats['transactions'] = dict()
|
||||||
|
for date in period.processed_transactions:
|
||||||
|
if start_dt <= date < end_dt:
|
||||||
|
stats['transactions'][date] = \
|
||||||
|
period.processed_transactions[date]
|
||||||
|
|
||||||
|
stats['orders'] = dict()
|
||||||
|
for date in period.orders_by_modified:
|
||||||
|
if start_dt <= date < end_dt:
|
||||||
|
stats['orders'][date] = \
|
||||||
|
period.orders_by_modified[date]
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def handle_data(self, data):
|
||||||
|
if not self.is_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._update_portfolio()
|
||||||
|
|
||||||
|
transactions = self._check_open_orders()
|
||||||
|
for transaction in transactions:
|
||||||
|
self.perf_tracker.process_transaction(transaction)
|
||||||
|
|
||||||
|
if self._handle_data:
|
||||||
|
self._handle_data(self, data)
|
||||||
|
|
||||||
|
# Unlike trading controls which remain constant unless placing an
|
||||||
|
# order, account controls can change each bar. Thus, must check
|
||||||
|
# every bar no matter if the algorithm places an order or not.
|
||||||
|
self.validate_account_controls()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Since the clock runs 24/7, I trying to disable the daily
|
||||||
|
# Performance tracker and keep only minute and cumulative
|
||||||
|
self.perf_tracker.update_performance()
|
||||||
|
|
||||||
|
# TODO: save for future use?
|
||||||
|
minute_stats = self.prepare_period_stats(
|
||||||
|
data.current_dt, data.current_dt + timedelta(minutes=1))
|
||||||
|
log.debug('the minute performance:\n{}'.format(minute_stats))
|
||||||
|
|
||||||
|
today = pd.to_datetime('today', utc=True)
|
||||||
|
daily_stats = self.prepare_period_stats(
|
||||||
|
start_dt=today,
|
||||||
|
end_dt=pd.Timestamp.utcnow()
|
||||||
|
)
|
||||||
|
save_algo_object(
|
||||||
|
algo_name=self.algo_namespace,
|
||||||
|
key=today.strftime('%Y-%m-%d'),
|
||||||
|
obj=daily_stats,
|
||||||
|
rel_path='daily_perf'
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('unable to calculate performance: {}'.format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
save_algo_object(
|
||||||
|
algo_name=self.algo_namespace,
|
||||||
|
key='perf_tracker',
|
||||||
|
obj=self.perf_tracker
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('unable to save minute perfs to disk: {}'.format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
save_algo_object(
|
||||||
|
algo_name=self.algo_namespace,
|
||||||
|
key='portfolio_{}'.format(self.exchange.name),
|
||||||
|
obj=self.exchange.portfolio
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.warn('unable to save portfolio to disk: {}'.format(e))
|
||||||
|
|
||||||
|
def _order(self,
|
||||||
|
asset,
|
||||||
|
amount,
|
||||||
|
limit_price=None,
|
||||||
|
stop_price=None,
|
||||||
|
style=None,
|
||||||
|
attempt_index=0):
|
||||||
|
try:
|
||||||
|
return self.exchange.order(asset, amount, limit_price,
|
||||||
|
stop_price,
|
||||||
|
style)
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
|
'order attempt {}: {}'.format(attempt_index, e)
|
||||||
|
)
|
||||||
|
if attempt_index < self.retry_order:
|
||||||
|
sleep(self.retry_delay)
|
||||||
|
return self._order(
|
||||||
|
asset, amount, limit_price, stop_price, style,
|
||||||
|
attempt_index + 1)
|
||||||
|
else:
|
||||||
|
raise ExchangeTransactionError(
|
||||||
|
transaction_type='order',
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
|
@api_method
|
||||||
|
@disallowed_in_before_trading_start(OrderInBeforeTradingStart())
|
||||||
|
def order(self,
|
||||||
|
asset,
|
||||||
|
amount,
|
||||||
|
limit_price=None,
|
||||||
|
stop_price=None,
|
||||||
|
style=None):
|
||||||
|
amount, style = self._calculate_order(asset, amount,
|
||||||
|
limit_price, stop_price,
|
||||||
|
style)
|
||||||
|
|
||||||
|
order_id = self._order(asset, amount, limit_price, stop_price, style)
|
||||||
|
order = self.portfolio.open_orders[order_id]
|
||||||
|
|
||||||
|
self.perf_tracker.process_order(order)
|
||||||
|
return order
|
||||||
|
|
||||||
|
def round_order(self, amount):
|
||||||
|
"""
|
||||||
|
We need fractions with cryptocurrencies
|
||||||
|
|
||||||
|
:param amount:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return amount
|
||||||
|
|
||||||
|
@api_method
|
||||||
|
def batch_market_order(self, share_counts):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def _get_open_orders(self, asset=None, attempt_index=0):
|
||||||
|
try:
|
||||||
|
return self.exchange.get_open_orders(asset)
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
|
'open orders attempt {}: {}'.format(attempt_index, e)
|
||||||
|
)
|
||||||
|
if attempt_index < self.retry_get_open_orders:
|
||||||
|
sleep(self.retry_delay)
|
||||||
|
return self._get_open_orders(asset, attempt_index + 1)
|
||||||
|
else:
|
||||||
|
raise ExchangePortfolioDataError(
|
||||||
|
data_type='open-orders',
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
|
@error_keywords(sid='Keyword argument `sid` is no longer supported for '
|
||||||
|
'get_open_orders. Use `asset` instead.')
|
||||||
|
@api_method
|
||||||
|
def get_open_orders(self, asset=None):
|
||||||
|
return self._get_open_orders(asset)
|
||||||
|
|
||||||
|
@api_method
|
||||||
|
def get_order(self, order_id):
|
||||||
|
return self.exchange.get_order(order_id)
|
||||||
|
|
||||||
|
@api_method
|
||||||
|
def cancel_order(self, order_param):
|
||||||
|
order_id = order_param
|
||||||
|
if isinstance(order_param, zp.Order):
|
||||||
|
order_id = order_param.id
|
||||||
|
self.exchange.cancel_order(order_id)
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = Logger('AssetFinderExchange')
|
||||||
|
|
||||||
log = Logger('AssetFinderExchange', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class AssetFinderExchange(object):
|
class AssetFinderExchange(object):
|
||||||
def __init__(self):
|
def __init__(self, exchange):
|
||||||
|
self.exchange = exchange
|
||||||
self._asset_cache = {}
|
self._asset_cache = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -41,15 +40,14 @@ class AssetFinderExchange(object):
|
|||||||
SidsNotFound
|
SidsNotFound
|
||||||
When a requested sid is not found and default_none=False.
|
When a requested sid is not found and default_none=False.
|
||||||
"""
|
"""
|
||||||
# for sid in sids:
|
for sid in sids:
|
||||||
# if sid in self._asset_cache:
|
if sid in self._asset_cache:
|
||||||
# log.debug('got asset from cache: {}'.format(sid))
|
log.info('got asset from cache: {}'.format(sid))
|
||||||
# else:
|
else:
|
||||||
# log.debug('fetching asset: {}'.format(sid))
|
log.info('fetching asset: {}'.format(sid))
|
||||||
return list()
|
return list()
|
||||||
|
|
||||||
def lookup_symbol(self, symbol, exchange, data_frequency=None,
|
def lookup_symbol(self, symbol, as_of_date, fuzzy=False):
|
||||||
as_of_date=None, fuzzy=False):
|
|
||||||
"""Lookup an asset by symbol.
|
"""Lookup an asset by symbol.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -83,17 +81,11 @@ class AssetFinderExchange(object):
|
|||||||
there are multiple candidates for the given ``symbol`` on the
|
there are multiple candidates for the given ``symbol`` on the
|
||||||
``as_of_date``.
|
``as_of_date``.
|
||||||
"""
|
"""
|
||||||
log.debug('looking up symbol: {} {}'.format(symbol, exchange.name))
|
log.info('looking up symbol: {}'.format(symbol))
|
||||||
|
|
||||||
if data_frequency is not None:
|
|
||||||
key = ','.join([exchange.name, symbol, data_frequency])
|
|
||||||
|
|
||||||
|
if symbol in self._asset_cache:
|
||||||
|
return self._asset_cache[symbol]
|
||||||
else:
|
else:
|
||||||
key = ','.join([exchange.name, symbol])
|
asset = self.exchange.get_asset(symbol)
|
||||||
|
self._asset_cache[symbol] = asset
|
||||||
if key in self._asset_cache:
|
|
||||||
return self._asset_cache[key]
|
|
||||||
else:
|
|
||||||
asset = exchange.get_asset(symbol, data_frequency)
|
|
||||||
self._asset_cache[key] = asset
|
|
||||||
return asset
|
return asset
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
import base64
|
import base64
|
||||||
import datetime
|
import numpy as np
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytz
|
import pytz
|
||||||
import requests
|
import requests
|
||||||
import six
|
import six
|
||||||
from catalyst.assets._assets import TradingPair
|
from catalyst.assets._assets import Asset
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
|
# from websocket import create_connection
|
||||||
from catalyst.exchange.exchange import Exchange
|
from catalyst.exchange.exchange import Exchange
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
|
||||||
from catalyst.exchange.exchange_errors import (
|
from catalyst.exchange.exchange_errors import (
|
||||||
ExchangeRequestError,
|
ExchangeRequestError,
|
||||||
InvalidHistoryFrequencyError,
|
InvalidHistoryFrequencyError
|
||||||
InvalidOrderStyle, OrderCancelError)
|
)
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
|
from catalyst.finance.execution import (MarketOrder,
|
||||||
ExchangeStopLimitOrder, ExchangeStopOrder
|
LimitOrder,
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
|
StopOrder,
|
||||||
download_exchange_symbols, get_symbols_string
|
StopLimitOrder)
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
from catalyst.protocol import Account
|
from catalyst.protocol import Account
|
||||||
|
|
||||||
@@ -33,9 +32,7 @@ requests.adapters.DEFAULT_RETRIES = 20
|
|||||||
|
|
||||||
BITFINEX_URL = 'https://api.bitfinex.com'
|
BITFINEX_URL = 'https://api.bitfinex.com'
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = Logger('Bitfinex')
|
||||||
|
|
||||||
log = Logger('Bitfinex', level=LOG_LEVEL)
|
|
||||||
warning_logger = Logger('AlgoWarning')
|
warning_logger = Logger('AlgoWarning')
|
||||||
|
|
||||||
|
|
||||||
@@ -43,31 +40,16 @@ class Bitfinex(Exchange):
|
|||||||
def __init__(self, key, secret, base_currency, portfolio=None):
|
def __init__(self, key, secret, base_currency, portfolio=None):
|
||||||
self.url = BITFINEX_URL
|
self.url = BITFINEX_URL
|
||||||
self.key = key
|
self.key = key
|
||||||
self.secret = secret.encode('UTF-8')
|
self.secret = secret
|
||||||
|
self.id = 'b'
|
||||||
self.name = 'bitfinex'
|
self.name = 'bitfinex'
|
||||||
self.color = 'green'
|
self.assets = {}
|
||||||
|
|
||||||
self.assets = dict()
|
|
||||||
self.load_assets()
|
self.load_assets()
|
||||||
|
|
||||||
self.local_assets = dict()
|
|
||||||
self.load_assets(is_local=True)
|
|
||||||
|
|
||||||
self.base_currency = base_currency
|
self.base_currency = base_currency
|
||||||
self._portfolio = portfolio
|
self._portfolio = portfolio
|
||||||
self.minute_writer = None
|
self.minute_writer = None
|
||||||
self.minute_reader = 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.name)
|
|
||||||
|
|
||||||
def _request(self, operation, data, version='v1'):
|
def _request(self, operation, data, version='v1'):
|
||||||
payload_object = {
|
payload_object = {
|
||||||
'request': '/{}/{}'.format(version, operation),
|
'request': '/{}/{}'.format(version, operation),
|
||||||
@@ -153,10 +135,7 @@ class Bitfinex(Exchange):
|
|||||||
|
|
||||||
amount = float(order_status['original_amount'])
|
amount = float(order_status['original_amount'])
|
||||||
filled = float(order_status['executed_amount'])
|
filled = float(order_status['executed_amount'])
|
||||||
|
is_buy = (amount > 0)
|
||||||
if order_status['side'] == 'sell':
|
|
||||||
amount = -amount
|
|
||||||
filled = -filled
|
|
||||||
|
|
||||||
price = float(order_status['price'])
|
price = float(order_status['price'])
|
||||||
order_type = order_status['type']
|
order_type = order_status['type']
|
||||||
@@ -175,6 +154,7 @@ class Bitfinex(Exchange):
|
|||||||
# TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it.
|
# TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it.
|
||||||
commission = None
|
commission = None
|
||||||
|
|
||||||
|
# TODO: zipline likes rounded dates to match statistics, is this ok?
|
||||||
date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
|
date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
|
||||||
date = pytz.utc.localize(date)
|
date = pytz.utc.localize(date)
|
||||||
order = Order(
|
order = Order(
|
||||||
@@ -184,17 +164,21 @@ class Bitfinex(Exchange):
|
|||||||
stop=stop_price,
|
stop=stop_price,
|
||||||
limit=limit_price,
|
limit=limit_price,
|
||||||
filled=filled,
|
filled=filled,
|
||||||
id=str(order_status['id']),
|
id=order_status['id'],
|
||||||
commission=commission
|
commission=commission
|
||||||
)
|
)
|
||||||
order.status = status
|
order.status = status
|
||||||
|
|
||||||
return order, executed_price
|
return order, executed_price
|
||||||
|
|
||||||
def get_balances(self):
|
def update_portfolio(self):
|
||||||
log.debug('retrieving wallets balances')
|
"""
|
||||||
|
Update the portfolio cash and position balances based on the
|
||||||
|
latest ticker prices.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.ask_request()
|
|
||||||
response = self._request('balances', None)
|
response = self._request('balances', None)
|
||||||
balances = response.json()
|
balances = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -205,12 +189,56 @@ class Bitfinex(Exchange):
|
|||||||
error='unable to fetch balance {}'.format(balances['message'])
|
error='unable to fetch balance {}'.format(balances['message'])
|
||||||
)
|
)
|
||||||
|
|
||||||
std_balances = dict()
|
base_position = None
|
||||||
for balance in balances:
|
for position in balances:
|
||||||
currency = balance['currency'].lower()
|
if not base_position and position['type'] == 'exchange' \
|
||||||
std_balances[currency] = float(balance['available'])
|
and position['currency'] == self.base_currency:
|
||||||
|
base_position = position
|
||||||
|
|
||||||
return std_balances
|
if position is None:
|
||||||
|
raise ValueError(
|
||||||
|
error='Base currency %s not found in portfolio' % self.base_currency
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio = self._portfolio
|
||||||
|
portfolio.cash = float(base_position['available'])
|
||||||
|
if portfolio.starting_cash is None:
|
||||||
|
portfolio.starting_cash = portfolio.cash
|
||||||
|
|
||||||
|
if portfolio.positions:
|
||||||
|
assets = portfolio.positions.keys()
|
||||||
|
tickers = self.tickers(assets)
|
||||||
|
portfolio.positions_value = 0.0
|
||||||
|
for ticker in tickers:
|
||||||
|
# TODO: convert if the position is not in the base currency
|
||||||
|
position = portfolio.positions[ticker['asset']]
|
||||||
|
position.last_sale_price = ticker['last_price']
|
||||||
|
position.last_sale_date = ticker['timestamp']
|
||||||
|
|
||||||
|
portfolio.positions_value += \
|
||||||
|
position.amount * position.last_sale_price
|
||||||
|
portfolio.portfolio_value = \
|
||||||
|
portfolio.positions_value + portfolio.cash
|
||||||
|
|
||||||
|
@property
|
||||||
|
def portfolio(self):
|
||||||
|
"""
|
||||||
|
Return the Portfolio
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# if self._portfolio is None:
|
||||||
|
# portfolio = ExchangePortfolio(
|
||||||
|
# start_date=pd.Timestamp.utcnow()
|
||||||
|
# )
|
||||||
|
# self.store.portfolio = portfolio
|
||||||
|
# self.update_portfolio()
|
||||||
|
#
|
||||||
|
# portfolio.starting_cash = portfolio.cash
|
||||||
|
# else:
|
||||||
|
# portfolio = self.store.portfolio
|
||||||
|
|
||||||
|
return self._portfolio
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account(self):
|
def account(self):
|
||||||
@@ -236,17 +264,19 @@ class Bitfinex(Exchange):
|
|||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
@property
|
||||||
|
def positions(self):
|
||||||
|
return self.portfolio.positions
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def time_skew(self):
|
def time_skew(self):
|
||||||
# TODO: research the time skew conditions
|
# TODO: research the time skew conditions
|
||||||
return pd.Timedelta('0s')
|
return pd.Timedelta('0s')
|
||||||
|
|
||||||
def get_account(self):
|
def subscribe_to_market_data(self, symbol):
|
||||||
# TODO: fetch account data and keep in cache
|
pass
|
||||||
return None
|
|
||||||
|
|
||||||
def get_candles(self, freq, assets, bar_count=None,
|
def get_candles(self, data_frequency, assets, bar_count=None):
|
||||||
start_dt=None, end_dt=None):
|
|
||||||
"""
|
"""
|
||||||
Retrieve OHLVC candles from Bitfinex
|
Retrieve OHLVC candles from Bitfinex
|
||||||
|
|
||||||
@@ -260,44 +290,38 @@ class Bitfinex(Exchange):
|
|||||||
'1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D', '14D',
|
'1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D', '14D',
|
||||||
'1M'
|
'1M'
|
||||||
"""
|
"""
|
||||||
log.debug(
|
|
||||||
'retrieving {bars} {freq} candles on {exchange} from '
|
|
||||||
'{end_dt} for markets {symbols}, '.format(
|
|
||||||
bars=bar_count,
|
|
||||||
freq=freq,
|
|
||||||
exchange=self.name,
|
|
||||||
end_dt=end_dt,
|
|
||||||
symbols=get_symbols_string(assets)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
allowed_frequencies = ['1T', '5T', '15T', '30T', '60T', '180T',
|
# TODO: use BcolzMinuteBarReader to read from cache
|
||||||
'360T', '720T', '1D', '7D', '14D', '30D']
|
freq_match = re.match(r'([0-9].*)(m|h|d)', data_frequency, re.M | re.I)
|
||||||
if freq not in allowed_frequencies:
|
|
||||||
raise InvalidHistoryFrequencyError(frequency=freq)
|
|
||||||
|
|
||||||
freq_match = re.match(r'([0-9].*)(T|H|D)', freq, re.M | re.I)
|
|
||||||
if freq_match:
|
if freq_match:
|
||||||
number = int(freq_match.group(1))
|
number = int(freq_match.group(1))
|
||||||
unit = freq_match.group(2)
|
unit = freq_match.group(2)
|
||||||
|
|
||||||
if unit == 'T':
|
if unit == 'd':
|
||||||
if number in [60, 180, 360, 720]:
|
converted_unit = 'D'
|
||||||
number = number / 60
|
|
||||||
converted_unit = 'h'
|
|
||||||
else:
|
|
||||||
converted_unit = 'm'
|
|
||||||
else:
|
else:
|
||||||
converted_unit = unit
|
converted_unit = unit
|
||||||
|
|
||||||
frequency = '{}{}'.format(number, converted_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:
|
else:
|
||||||
raise InvalidHistoryFrequencyError(frequency=freq)
|
raise InvalidHistoryFrequencyError(
|
||||||
|
frequency=data_frequency
|
||||||
|
)
|
||||||
|
|
||||||
# Making sure that assets are iterable
|
# Making sure that assets are iterable
|
||||||
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
asset_list = [assets] if isinstance(assets, Asset) else assets
|
||||||
ohlc_map = dict()
|
ohlc_list = dict()
|
||||||
for asset in asset_list:
|
for asset in asset_list:
|
||||||
symbol = self._get_v2_symbol(asset)
|
symbol = self._get_v2_symbol(asset)
|
||||||
url = '{url}/v2/candles/trade:{frequency}:{symbol}'.format(
|
url = '{url}/v2/candles/trade:{frequency}:{symbol}'.format(
|
||||||
@@ -309,27 +333,11 @@ class Bitfinex(Exchange):
|
|||||||
if bar_count:
|
if bar_count:
|
||||||
is_list = True
|
is_list = True
|
||||||
url += '/hist?limit={}'.format(int(bar_count))
|
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:
|
else:
|
||||||
is_list = False
|
is_list = False
|
||||||
url += '/last'
|
url += '/last'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.ask_request()
|
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
@@ -343,9 +351,6 @@ class Bitfinex(Exchange):
|
|||||||
candles = response.json()
|
candles = response.json()
|
||||||
|
|
||||||
def ohlc_from_candle(candle):
|
def ohlc_from_candle(candle):
|
||||||
last_traded = pd.Timestamp.utcfromtimestamp(
|
|
||||||
candle[0] / 1000.0)
|
|
||||||
last_traded = last_traded.replace(tzinfo=pytz.UTC)
|
|
||||||
ohlc = dict(
|
ohlc = dict(
|
||||||
open=np.float64(candle[1]),
|
open=np.float64(candle[1]),
|
||||||
high=np.float64(candle[3]),
|
high=np.float64(candle[3]),
|
||||||
@@ -353,7 +358,9 @@ class Bitfinex(Exchange):
|
|||||||
close=np.float64(candle[2]),
|
close=np.float64(candle[2]),
|
||||||
volume=np.float64(candle[5]),
|
volume=np.float64(candle[5]),
|
||||||
price=np.float64(candle[2]),
|
price=np.float64(candle[2]),
|
||||||
last_traded=last_traded
|
last_traded=pd.Timestamp.utcfromtimestamp(
|
||||||
|
candle[0] / 1000.0),
|
||||||
|
minute_dt=pd.Timestamp.utcnow().floor('1 min')
|
||||||
)
|
)
|
||||||
return ohlc
|
return ohlc
|
||||||
|
|
||||||
@@ -364,43 +371,102 @@ class Bitfinex(Exchange):
|
|||||||
ohlc = ohlc_from_candle(candle)
|
ohlc = ohlc_from_candle(candle)
|
||||||
ohlc_bars.append(ohlc)
|
ohlc_bars.append(ohlc)
|
||||||
|
|
||||||
ohlc_map[asset] = ohlc_bars
|
ohlc_list[asset] = ohlc_bars
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ohlc = ohlc_from_candle(candles)
|
ohlc = ohlc_from_candle(candles)
|
||||||
ohlc_map[asset] = ohlc
|
ohlc_list[asset] = ohlc
|
||||||
|
|
||||||
return ohlc_map[assets] \
|
return ohlc_list[assets] \
|
||||||
if isinstance(assets, TradingPair) else ohlc_map
|
if isinstance(assets, Asset) else ohlc_list
|
||||||
|
|
||||||
def create_order(self, asset, amount, is_buy, style):
|
def order(self, asset, amount, limit_price, stop_price, style):
|
||||||
|
"""Place an order.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset : Asset
|
||||||
|
The asset that this order is for.
|
||||||
|
amount : int
|
||||||
|
The amount of shares to order. If ``amount`` is positive, this is
|
||||||
|
the number of shares to buy or cover. If ``amount`` is negative,
|
||||||
|
this is the number of shares to sell or short.
|
||||||
|
limit_price : float, optional
|
||||||
|
The limit price for the order.
|
||||||
|
stop_price : float, optional
|
||||||
|
The stop price for the order.
|
||||||
|
style : ExecutionStyle, optional
|
||||||
|
The execution style for the order.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
order_id : str or None
|
||||||
|
The unique identifier for this order, or None if no order was
|
||||||
|
placed.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
The ``limit_price`` and ``stop_price`` arguments provide shorthands for
|
||||||
|
passing common execution styles. Passing ``limit_price=N`` is
|
||||||
|
equivalent to ``style=LimitOrder(N)``. Similarly, passing
|
||||||
|
``stop_price=M`` is equivalent to ``style=StopOrder(M)``, and passing
|
||||||
|
``limit_price=N`` and ``stop_price=M`` is equivalent to
|
||||||
|
``style=StopLimitOrder(N, M)``. It is an error to pass both a ``style``
|
||||||
|
and ``limit_price`` or ``stop_price``.
|
||||||
|
|
||||||
|
Bitfinex Order Types
|
||||||
|
--------------------
|
||||||
|
LIMIT, MARKET, STOP, TRAILING STOP,
|
||||||
|
EXCHANGE MARKET, EXCHANGE LIMIT, EXCHANGE STOP,
|
||||||
|
EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
:class:`catalyst.finance.execution.ExecutionStyle`
|
||||||
|
:func:`catalyst.api.order_value`
|
||||||
|
:func:`catalyst.api.order_percent`
|
||||||
"""
|
"""
|
||||||
Creating order on the exchange.
|
if amount == 0:
|
||||||
|
log.warn('skipping order amount of 0')
|
||||||
|
return None
|
||||||
|
|
||||||
:param asset:
|
base_currency = asset.symbol.split('_')[1]
|
||||||
:param amount:
|
if base_currency.lower() != self.base_currency.lower():
|
||||||
:param is_buy:
|
raise NotImplementedError(
|
||||||
:param style:
|
'Currency pairs must share their base with the exchange.'
|
||||||
:return:
|
)
|
||||||
"""
|
|
||||||
exchange_symbol = self.get_symbol(asset)
|
is_buy = (amount > 0)
|
||||||
if isinstance(style, ExchangeLimitOrder) \
|
|
||||||
or isinstance(style, ExchangeStopLimitOrder):
|
if isinstance(style, MarketOrder):
|
||||||
price = style.get_limit_price(is_buy)
|
order_type = 'market'
|
||||||
|
elif isinstance(style, LimitOrder):
|
||||||
order_type = 'limit'
|
order_type = 'limit'
|
||||||
|
price = limit_price
|
||||||
elif isinstance(style, ExchangeStopOrder):
|
elif isinstance(style, StopOrder):
|
||||||
price = style.get_stop_price(is_buy)
|
|
||||||
order_type = 'stop'
|
order_type = 'stop'
|
||||||
|
price = stop_price
|
||||||
|
elif isinstance(style, StopLimitOrder):
|
||||||
|
log.warn('using limit order instead of stop/limit')
|
||||||
|
# TODO: Not sure how to do this with the api. Investigate.
|
||||||
|
order_type = 'limit'
|
||||||
|
price = limit_price
|
||||||
else:
|
else:
|
||||||
raise InvalidOrderStyle(exchange=self.name,
|
raise NotImplementedError('%s orders not available' % style)
|
||||||
style=style.__class__.__name__)
|
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
'ordering {amount} {symbol} for {price}'.format(
|
||||||
|
amount=amount,
|
||||||
|
symbol=asset.symbol,
|
||||||
|
price=price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
exchange_symbol = self.get_symbol(asset)
|
||||||
req = dict(
|
req = dict(
|
||||||
symbol=exchange_symbol,
|
symbol=exchange_symbol,
|
||||||
amount=str(float(abs(amount))),
|
amount=str(float(abs(amount))),
|
||||||
price="{:.20f}".format(float(price)),
|
price=str(float(price)),
|
||||||
side='buy' if is_buy else 'sell',
|
side='buy' if is_buy else 'sell',
|
||||||
type='exchange ' + order_type, # TODO: support margin trades
|
type='exchange ' + order_type, # TODO: support margin trades
|
||||||
exchange=self.name,
|
exchange=self.name,
|
||||||
@@ -414,19 +480,18 @@ class Bitfinex(Exchange):
|
|||||||
|
|
||||||
date = pd.Timestamp.utcnow()
|
date = pd.Timestamp.utcnow()
|
||||||
try:
|
try:
|
||||||
self.ask_request()
|
|
||||||
response = self._request('order/new', req)
|
response = self._request('order/new', req)
|
||||||
order_status = response.json()
|
exchange_order = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
if 'message' in order_status:
|
if 'message' in exchange_order:
|
||||||
raise ExchangeRequestError(
|
raise ExchangeRequestError(
|
||||||
error='unable to create Bitfinex order {}'.format(
|
error='unable to create Bitfinex order {}'.format(
|
||||||
order_status['message'])
|
exchange_order['message'])
|
||||||
)
|
)
|
||||||
|
|
||||||
order_id = str(order_status['id'])
|
order_id = exchange_order['id']
|
||||||
order = Order(
|
order = Order(
|
||||||
dt=date,
|
dt=date,
|
||||||
asset=asset,
|
asset=asset,
|
||||||
@@ -435,8 +500,12 @@ class Bitfinex(Exchange):
|
|||||||
limit=style.get_limit_price(is_buy),
|
limit=style.get_limit_price(is_buy),
|
||||||
id=order_id
|
id=order_id
|
||||||
)
|
)
|
||||||
|
# TODO: is this required?
|
||||||
|
order.broker_order_id = order_id
|
||||||
|
|
||||||
return order
|
self.portfolio.create_order(order)
|
||||||
|
|
||||||
|
return order_id
|
||||||
|
|
||||||
def get_open_orders(self, asset=None):
|
def get_open_orders(self, asset=None):
|
||||||
"""Retrieve all of the current open orders.
|
"""Retrieve all of the current open orders.
|
||||||
@@ -456,7 +525,6 @@ class Bitfinex(Exchange):
|
|||||||
orders for this asset.
|
orders for this asset.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.ask_request()
|
|
||||||
response = self._request('orders', None)
|
response = self._request('orders', None)
|
||||||
order_statuses = response.json()
|
order_statuses = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -468,9 +536,9 @@ class Bitfinex(Exchange):
|
|||||||
order_statuses['message'])
|
order_statuses['message'])
|
||||||
)
|
)
|
||||||
|
|
||||||
orders = []
|
orders = list()
|
||||||
for order_status in order_statuses:
|
for order_status in order_statuses:
|
||||||
order, executed_price = self._create_order(order_status)
|
order, = self._create_order(order_status)
|
||||||
if asset is None or asset == order.sid:
|
if asset is None or asset == order.sid:
|
||||||
orders.append(order)
|
orders.append(order)
|
||||||
|
|
||||||
@@ -491,7 +559,6 @@ class Bitfinex(Exchange):
|
|||||||
The order object.
|
The order object.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.ask_request()
|
|
||||||
response = self._request(
|
response = self._request(
|
||||||
'order/status', {'order_id': int(order_id)})
|
'order/status', {'order_id': int(order_id)})
|
||||||
order_status = response.json()
|
order_status = response.json()
|
||||||
@@ -517,17 +584,15 @@ class Bitfinex(Exchange):
|
|||||||
if isinstance(order_param, Order) else order_param
|
if isinstance(order_param, Order) else order_param
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.ask_request()
|
|
||||||
response = self._request('order/cancel', {'order_id': order_id})
|
response = self._request('order/cancel', {'order_id': order_id})
|
||||||
status = response.json()
|
status = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
if 'message' in status:
|
if 'message' in status:
|
||||||
raise OrderCancelError(
|
raise ExchangeRequestError(
|
||||||
order_id=order_id,
|
error='Unable to cancel order: {} {}'.format(
|
||||||
exchange=self.name,
|
order_id, status['message'])
|
||||||
error=status['message']
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def tickers(self, assets):
|
def tickers(self, assets):
|
||||||
@@ -542,7 +607,6 @@ class Bitfinex(Exchange):
|
|||||||
log.debug('fetching tickers {}'.format(symbols))
|
log.debug('fetching tickers {}'.format(symbols))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.ask_request()
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
'{url}/v2/tickers?symbols={symbols}'.format(
|
'{url}/v2/tickers?symbols={symbols}'.format(
|
||||||
url=self.url,
|
url=self.url,
|
||||||
@@ -558,19 +622,17 @@ class Bitfinex(Exchange):
|
|||||||
response.content)
|
response.content)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
tickers = response.json()
|
||||||
tickers = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
ticks = dict()
|
formatted_tickers = []
|
||||||
for index, ticker in enumerate(tickers):
|
for index, ticker in enumerate(tickers):
|
||||||
if not len(ticker) == 11:
|
if not len(ticker) == 11:
|
||||||
raise ExchangeRequestError(
|
raise ExchangeRequestError(
|
||||||
error='Invalid ticker in response: {}'.format(ticker)
|
error='Invalid ticker in response: {}'.format(ticker)
|
||||||
)
|
)
|
||||||
|
|
||||||
ticks[assets[index]] = dict(
|
tick = dict(
|
||||||
|
asset=assets[index],
|
||||||
timestamp=pd.Timestamp.utcnow(),
|
timestamp=pd.Timestamp.utcnow(),
|
||||||
bid=ticker[1],
|
bid=ticker[1],
|
||||||
ask=ticker[3],
|
ask=ticker[3],
|
||||||
@@ -579,127 +641,7 @@ class Bitfinex(Exchange):
|
|||||||
high=ticker[9],
|
high=ticker[9],
|
||||||
volume=ticker[8],
|
volume=ticker[8],
|
||||||
)
|
)
|
||||||
|
formatted_tickers.append(tick)
|
||||||
|
|
||||||
log.debug('got tickers {}'.format(ticks))
|
log.debug('got tickers {}'.format(formatted_tickers))
|
||||||
return ticks
|
return formatted_tickers
|
||||||
|
|
||||||
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,416 +0,0 @@
|
|||||||
import json
|
|
||||||
import time
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
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.exchange.exchange_utils import get_exchange_symbols_filename, \
|
|
||||||
download_exchange_symbols, get_symbols_string
|
|
||||||
from catalyst.finance.execution import LimitOrder, StopLimitOrder
|
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
|
||||||
|
|
||||||
# TODO: consider using this: https://github.com/mondeja/bittrex_v2
|
|
||||||
|
|
||||||
log = Logger('Bittrex', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
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)
|
|
||||||
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.local_assets = dict()
|
|
||||||
self.load_assets(is_local=True)
|
|
||||||
|
|
||||||
self.bundle = ExchangeBundle(self.name)
|
|
||||||
|
|
||||||
@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):
|
|
||||||
balances = self.api.getbalances()
|
|
||||||
try:
|
|
||||||
log.debug('retrieving wallet balances')
|
|
||||||
self.ask_request()
|
|
||||||
|
|
||||||
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, freq, assets, bar_count=None,
|
|
||||||
start_dt=None, end_dt=None):
|
|
||||||
"""
|
|
||||||
Supported Intervals
|
|
||||||
-------------------
|
|
||||||
day, oneMin, fiveMin, thirtyMin, hour
|
|
||||||
|
|
||||||
:param freq:
|
|
||||||
:param assets:
|
|
||||||
:param bar_count:
|
|
||||||
:param start_dt
|
|
||||||
:param end_dt
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO: this has no effect at the moment
|
|
||||||
if end_dt is None:
|
|
||||||
end_dt = pd.Timestamp.utcnow()
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
'retrieving {bars} {freq} candles on {exchange} from '
|
|
||||||
'{end_dt} for markets {symbols}, '.format(
|
|
||||||
bars=bar_count,
|
|
||||||
freq=freq,
|
|
||||||
exchange=self.name,
|
|
||||||
end_dt=end_dt,
|
|
||||||
symbols=get_symbols_string(assets)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if freq == '1T':
|
|
||||||
frequency = 'oneMin'
|
|
||||||
elif freq == '5T':
|
|
||||||
frequency = 'fiveMin'
|
|
||||||
elif freq == '30T':
|
|
||||||
frequency = 'thirtyMin'
|
|
||||||
elif freq == '60T':
|
|
||||||
frequency = 'hour'
|
|
||||||
elif freq == '1D':
|
|
||||||
frequency = 'day'
|
|
||||||
else:
|
|
||||||
raise InvalidHistoryFrequencyError(frequency=freq)
|
|
||||||
|
|
||||||
# Making sure that assets are iterable
|
|
||||||
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
|
||||||
for asset in asset_list:
|
|
||||||
end = int(time.mktime(end_dt.timetuple()))
|
|
||||||
url = '{url}/pub/market/GetTicks?marketName={symbol}' \
|
|
||||||
'&tickInterval={frequency}&_={end}'.format(
|
|
||||||
url=URL2,
|
|
||||||
symbol=self.get_symbol(asset),
|
|
||||||
frequency=frequency,
|
|
||||||
end=end
|
|
||||||
)
|
|
||||||
|
|
||||||
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))
|
|
||||||
ohlc_map = dict()
|
|
||||||
if bar_count is None:
|
|
||||||
ohlc_map[asset] = ohlc_from_candle(ordered_candles[0])
|
|
||||||
else:
|
|
||||||
# TODO: optimize
|
|
||||||
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,132 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
import ssl
|
|
||||||
|
|
||||||
# Workaround for backwards compatibility
|
|
||||||
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
|
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
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.encode('utf-8'),
|
|
||||||
url.encode('utf-8'),
|
|
||||||
hashlib.sha512).hexdigest()
|
|
||||||
headers = {'apisign': signature}
|
|
||||||
else:
|
|
||||||
headers = {}
|
|
||||||
|
|
||||||
req = urllib.request.Request(url, headers=headers)
|
|
||||||
response = json.loads(urlopen(
|
|
||||||
req, context=ssl._create_unverified_context()).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,359 +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.assets._assets import TradingPair
|
|
||||||
|
|
||||||
from catalyst.data.bundles.core import download_without_progress
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_bundles_folder, \
|
|
||||||
get_exchange_symbols
|
|
||||||
|
|
||||||
EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
|
|
||||||
API_URL = 'http://data.enigma.co/api/v1'
|
|
||||||
|
|
||||||
|
|
||||||
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_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_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(start_dt, end_dt, freq):
|
|
||||||
"""
|
|
||||||
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'
|
|
||||||
|
|
||||||
return pd.date_range(start_dt, end_dt, freq=freq)
|
|
||||||
|
|
||||||
|
|
||||||
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, end_dt, 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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
"""
|
|
||||||
return '{}-{:02d}'.format(dt.year, dt.month) if data_frequency == 'minute' \
|
|
||||||
else '{}'.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_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 as e:
|
|
||||||
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
|
|
||||||
@@ -1,600 +0,0 @@
|
|||||||
import re
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
import ccxt
|
|
||||||
import pandas as pd
|
|
||||||
from ccxt import ExchangeNotAvailable
|
|
||||||
from six import string_types
|
|
||||||
|
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
|
||||||
|
|
||||||
from catalyst.algorithm import MarketOrder
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.exchange.exchange import Exchange, ExchangeLimitOrder
|
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
|
||||||
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
|
||||||
ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle, \
|
|
||||||
ExchangeNotFoundError
|
|
||||||
from catalyst.exchange.exchange_utils import mixin_market_params, \
|
|
||||||
from_ms_timestamp
|
|
||||||
|
|
||||||
log = Logger('CCXT', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
SUPPORTED_EXCHANGES = dict(
|
|
||||||
binance=ccxt.binance,
|
|
||||||
bitfinex=ccxt.bitfinex,
|
|
||||||
bittrex=ccxt.bittrex,
|
|
||||||
poloniex=ccxt.poloniex,
|
|
||||||
bitmex=ccxt.bitmex,
|
|
||||||
gdax=ccxt.gdax,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CCXT(Exchange):
|
|
||||||
def __init__(self, exchange_name, key, secret, base_currency,
|
|
||||||
portfolio=None):
|
|
||||||
log.debug(
|
|
||||||
'finding {} in CCXT exchanges:\n{}'.format(
|
|
||||||
exchange_name, ccxt.exchanges
|
|
||||||
)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
# Making instantiation as explicit as possible for code tracking.
|
|
||||||
if exchange_name in SUPPORTED_EXCHANGES:
|
|
||||||
exchange_attr = SUPPORTED_EXCHANGES[exchange_name]
|
|
||||||
|
|
||||||
else:
|
|
||||||
exchange_attr = getattr(ccxt, exchange_name)
|
|
||||||
|
|
||||||
self.api = exchange_attr({
|
|
||||||
'apiKey': key,
|
|
||||||
'secret': secret,
|
|
||||||
})
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
|
||||||
|
|
||||||
self._symbol_maps = [None, None]
|
|
||||||
|
|
||||||
markets_symbols = self.api.load_markets()
|
|
||||||
log.debug('the markets:\n{}'.format(markets_symbols))
|
|
||||||
|
|
||||||
self.name = exchange_name
|
|
||||||
|
|
||||||
self.markets = self.api.fetch_markets()
|
|
||||||
self.load_assets()
|
|
||||||
|
|
||||||
self.base_currency = base_currency
|
|
||||||
self._portfolio = portfolio
|
|
||||||
self.transactions = defaultdict(list)
|
|
||||||
|
|
||||||
self.num_candles_limit = 2000
|
|
||||||
self.max_requests_per_minute = 60
|
|
||||||
self.request_cpt = dict()
|
|
||||||
|
|
||||||
self.bundle = ExchangeBundle(self.name)
|
|
||||||
|
|
||||||
def account(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def time_skew(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_market(self, symbol):
|
|
||||||
"""
|
|
||||||
The CCXT market.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
symbol:
|
|
||||||
The CCXT symbol.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
dict[str, Object]
|
|
||||||
|
|
||||||
"""
|
|
||||||
s = self.get_symbol(symbol)
|
|
||||||
market = next(
|
|
||||||
(market for market in self.markets if market['symbol'] == s),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
return market
|
|
||||||
|
|
||||||
def get_symbol(self, asset_or_symbol):
|
|
||||||
"""
|
|
||||||
The CCXT symbol.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
asset_or_symbol
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
symbol = asset_or_symbol if isinstance(
|
|
||||||
asset_or_symbol, string_types
|
|
||||||
) else asset_or_symbol.symbol
|
|
||||||
|
|
||||||
parts = symbol.split('_')
|
|
||||||
return '{}/{}'.format(parts[0].upper(), parts[1].upper())
|
|
||||||
|
|
||||||
def get_catalyst_symbol(self, 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 get_timeframe(self, freq):
|
|
||||||
"""
|
|
||||||
The CCXT timeframe from the Catalyst frequency.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
freq: str
|
|
||||||
The Catalyst frequency (Pandas convention)
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
|
|
||||||
if unit.lower() == 'd':
|
|
||||||
timeframe = '{}d'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit.lower() == 'm' or unit == 'T':
|
|
||||||
timeframe = '{}m'.format(candle_size)
|
|
||||||
|
|
||||||
elif unit.lower() == 'h' or unit == 'T':
|
|
||||||
timeframe = '{}h'.format(candle_size)
|
|
||||||
|
|
||||||
return timeframe
|
|
||||||
|
|
||||||
def get_candles(self, freq, assets, bar_count=None, start_dt=None,
|
|
||||||
end_dt=None):
|
|
||||||
symbols = self.get_symbols(assets)
|
|
||||||
timeframe = self.get_timeframe(freq)
|
|
||||||
delta = start_dt - pd.to_datetime('1970-1-1', utc=True)
|
|
||||||
ms = int(delta.total_seconds()) * 1000
|
|
||||||
|
|
||||||
candles = dict()
|
|
||||||
for asset in assets:
|
|
||||||
ohlcvs = self.api.fetch_ohlcv(
|
|
||||||
symbol=symbols[0],
|
|
||||||
timeframe=timeframe,
|
|
||||||
since=ms,
|
|
||||||
limit=bar_count,
|
|
||||||
params={}
|
|
||||||
)
|
|
||||||
|
|
||||||
candles[asset] = []
|
|
||||||
for ohlcv in ohlcvs:
|
|
||||||
candles[asset].append(dict(
|
|
||||||
last_traded=pd.to_datetime(ohlcv[0], unit='ms', utc=True),
|
|
||||||
open=ohlcv[1],
|
|
||||||
high=ohlcv[2],
|
|
||||||
low=ohlcv[3],
|
|
||||||
close=ohlcv[4],
|
|
||||||
volume=ohlcv[5]
|
|
||||||
))
|
|
||||||
|
|
||||||
return candles
|
|
||||||
|
|
||||||
def _fetch_symbol_map(self, is_local):
|
|
||||||
try:
|
|
||||||
return self.fetch_symbol_map(is_local)
|
|
||||||
except ExchangeSymbolsNotFound:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_asset_defs(self, market):
|
|
||||||
"""
|
|
||||||
The local and Catalyst definitions of the specified market.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
market: dict[str, Object]
|
|
||||||
The CCXT market dicts.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
dict[str, Object]
|
|
||||||
The asset definition.
|
|
||||||
|
|
||||||
"""
|
|
||||||
asset_defs = []
|
|
||||||
|
|
||||||
for is_local in (False, True):
|
|
||||||
asset_def = self.get_asset_def(market, is_local)
|
|
||||||
asset_defs.append((asset_def, is_local))
|
|
||||||
|
|
||||||
return asset_defs
|
|
||||||
|
|
||||||
def get_asset_def(self, market, is_local=False):
|
|
||||||
"""
|
|
||||||
The asset definition (in symbols.json files) corresponding
|
|
||||||
to the the specified market.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
market: dict[str, Object]
|
|
||||||
The CCXT market dict.
|
|
||||||
is_local
|
|
||||||
Whether to search in local or Catalyst asset definitions.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
dict[str, Object]
|
|
||||||
The asset definition.
|
|
||||||
|
|
||||||
"""
|
|
||||||
exchange_symbol = market['id']
|
|
||||||
|
|
||||||
symbol_map = self._fetch_symbol_map(is_local)
|
|
||||||
if symbol_map is not None:
|
|
||||||
assets_lower = {k.lower(): v for k, v in symbol_map.items()}
|
|
||||||
key = exchange_symbol.lower()
|
|
||||||
|
|
||||||
asset = assets_lower[key] if key in assets_lower else None
|
|
||||||
if asset is not None:
|
|
||||||
return asset
|
|
||||||
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def create_trading_pair(self, market, asset_def, is_local):
|
|
||||||
"""
|
|
||||||
Creating a TradingPair from market and asset data.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
market: dict[str, Object]
|
|
||||||
asset_def: dict[str, Object]
|
|
||||||
is_local: bool
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
data_source = 'local' if is_local else 'catalyst'
|
|
||||||
params = dict(
|
|
||||||
exchange=self.name,
|
|
||||||
data_source=data_source,
|
|
||||||
exchange_symbol=market['id'],
|
|
||||||
)
|
|
||||||
mixin_market_params(self.name, params, market)
|
|
||||||
|
|
||||||
if asset_def is not None:
|
|
||||||
params['symbol'] = asset_def['symbol']
|
|
||||||
|
|
||||||
params['start_date'] = asset_def['start_date'] \
|
|
||||||
if 'start_date' in asset_def else None
|
|
||||||
|
|
||||||
params['end_date'] = asset_def['end_date'] \
|
|
||||||
if 'end_date' in asset_def else None
|
|
||||||
|
|
||||||
params['leverage'] = asset_def['leverage'] \
|
|
||||||
if 'leverage' in asset_def else 1.0
|
|
||||||
|
|
||||||
params['asset_name'] = asset_def['asset_name'] \
|
|
||||||
if 'asset_name' in asset_def else None
|
|
||||||
|
|
||||||
params['end_daily'] = asset_def['end_daily'] \
|
|
||||||
if 'end_daily' in asset_def \
|
|
||||||
and asset_def['end_daily'] != 'N/A' else None
|
|
||||||
|
|
||||||
params['end_minute'] = asset_def['end_minute'] \
|
|
||||||
if 'end_minute' in asset_def \
|
|
||||||
and asset_def['end_minute'] != 'N/A' else None
|
|
||||||
|
|
||||||
else:
|
|
||||||
params['symbol'] = self.get_catalyst_symbol(market)
|
|
||||||
# TODO: add as an optional column
|
|
||||||
params['leverage'] = 1.0
|
|
||||||
|
|
||||||
return TradingPair(**params)
|
|
||||||
|
|
||||||
def load_assets(self):
|
|
||||||
self.assets = []
|
|
||||||
|
|
||||||
for market in self.markets:
|
|
||||||
asset_defs = self.get_asset_defs(market)
|
|
||||||
|
|
||||||
for asset_def in asset_defs:
|
|
||||||
if asset_def[0] is not None or not asset_defs[1]:
|
|
||||||
try:
|
|
||||||
asset = self.create_trading_pair(
|
|
||||||
market=market,
|
|
||||||
asset_def=asset_def[0],
|
|
||||||
is_local=asset_def[1]
|
|
||||||
)
|
|
||||||
self.assets.append(asset)
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_balances(self):
|
|
||||||
try:
|
|
||||||
log.debug('retrieving wallets balances')
|
|
||||||
balances = self.api.fetch_balance()
|
|
||||||
|
|
||||||
balances_lower = dict()
|
|
||||||
for key in balances:
|
|
||||||
balances_lower[key.lower()] = balances[key]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.debug('error retrieving balances: {}', e)
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
return balances_lower
|
|
||||||
|
|
||||||
def _create_order(self, order_status):
|
|
||||||
"""
|
|
||||||
Create a Catalyst order object from a CCXT order dictionary
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
order_status: dict[str, Object]
|
|
||||||
The order dict from the CCXT api.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Order
|
|
||||||
The Catalyst order object
|
|
||||||
|
|
||||||
"""
|
|
||||||
if order_status['status'] == 'canceled':
|
|
||||||
status = ORDER_STATUS.CANCELLED
|
|
||||||
|
|
||||||
elif order_status['status'] == 'closed' and order_status['filled'] > 0:
|
|
||||||
log.debug('found executed order {}'.format(order_status))
|
|
||||||
status = ORDER_STATUS.FILLED
|
|
||||||
|
|
||||||
elif order_status['status'] == 'open':
|
|
||||||
status = ORDER_STATUS.OPEN
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError('invalid state for order')
|
|
||||||
|
|
||||||
amount = order_status['amount']
|
|
||||||
filled = order_status['filled']
|
|
||||||
|
|
||||||
if order_status['side'] == 'sell':
|
|
||||||
amount = -amount
|
|
||||||
filled = -filled
|
|
||||||
|
|
||||||
price = order_status['price']
|
|
||||||
order_type = order_status['type']
|
|
||||||
|
|
||||||
limit_price = price if order_type == 'limit' else None
|
|
||||||
stop_price = None # TODO: add support
|
|
||||||
|
|
||||||
executed_price = order_status['cost'] / order_status['amount']
|
|
||||||
commission = order_status['fee']
|
|
||||||
date = from_ms_timestamp(order_status['timestamp'])
|
|
||||||
|
|
||||||
# order_id = str(order_status['info']['clientOrderId'])
|
|
||||||
order_id = order_status['id']
|
|
||||||
|
|
||||||
# TODO: this won't work, redo the packages with a different key.
|
|
||||||
symbol = order_status['info']['symbol'] \
|
|
||||||
if 'symbol' in order_status['info'] \
|
|
||||||
else order_status['info']['Exchange']
|
|
||||||
|
|
||||||
order = Order(
|
|
||||||
dt=date,
|
|
||||||
asset=self.get_asset(symbol, is_exchange_symbol=True),
|
|
||||||
amount=amount,
|
|
||||||
stop=stop_price,
|
|
||||||
limit=limit_price,
|
|
||||||
filled=filled,
|
|
||||||
id=order_id,
|
|
||||||
commission=commission
|
|
||||||
)
|
|
||||||
order.status = status
|
|
||||||
|
|
||||||
return order, executed_price
|
|
||||||
|
|
||||||
def create_order(self, asset, amount, is_buy, style):
|
|
||||||
symbol = self.get_symbol(asset)
|
|
||||||
|
|
||||||
if isinstance(style, ExchangeLimitOrder):
|
|
||||||
price = style.get_limit_price(is_buy)
|
|
||||||
order_type = 'limit'
|
|
||||||
|
|
||||||
elif isinstance(style, MarketOrder):
|
|
||||||
price = None
|
|
||||||
order_type = 'market'
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise InvalidOrderStyle(
|
|
||||||
exchange=self.name,
|
|
||||||
style=style.__class__.__name__
|
|
||||||
)
|
|
||||||
|
|
||||||
side = 'buy' if amount > 0 else 'sell'
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.api.create_order(
|
|
||||||
symbol=symbol,
|
|
||||||
type=order_type,
|
|
||||||
side=side,
|
|
||||||
amount=abs(amount),
|
|
||||||
price=price
|
|
||||||
)
|
|
||||||
except ExchangeNotAvailable as e:
|
|
||||||
log.debug('unable to create order: {}'.format(e))
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
if 'info' not in result:
|
|
||||||
raise ValueError('cannot use order without info attribute')
|
|
||||||
|
|
||||||
order_id = result['id']
|
|
||||||
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
|
|
||||||
|
|
||||||
def get_open_orders(self, asset):
|
|
||||||
try:
|
|
||||||
symbol = self.get_symbol(asset)
|
|
||||||
result = self.api.fetch_open_orders(
|
|
||||||
symbol=symbol,
|
|
||||||
since=None,
|
|
||||||
limit=None,
|
|
||||||
params=dict()
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
orders = []
|
|
||||||
for order_status in result:
|
|
||||||
order, executed_price = self._create_order(order_status)
|
|
||||||
if asset is None or asset == order.sid:
|
|
||||||
orders.append(order)
|
|
||||||
|
|
||||||
return orders
|
|
||||||
|
|
||||||
def _get_asset_from_order(self, order_id):
|
|
||||||
open_orders = self.portfolio.open_orders
|
|
||||||
order = next(
|
|
||||||
(open_orders[id] for id in open_orders if id == order_id),
|
|
||||||
None
|
|
||||||
) # type: Order
|
|
||||||
return order.asset if order is not None else None
|
|
||||||
|
|
||||||
def get_order(self, order_id, asset_or_symbol=None):
|
|
||||||
if asset_or_symbol is None and self.portfolio is not None:
|
|
||||||
asset_or_symbol = self._get_asset_from_order(order_id)
|
|
||||||
|
|
||||||
if asset_or_symbol is None:
|
|
||||||
log.debug(
|
|
||||||
'order not found in memory, the request might fail '
|
|
||||||
'on some exchanges.'
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
symbol = self.get_symbol(asset_or_symbol) \
|
|
||||||
if asset_or_symbol is not None else None
|
|
||||||
order_status = self.api.fetch_order(id=order_id, symbol=symbol)
|
|
||||||
order, executed_price = self._create_order(order_status)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
return order, executed_price
|
|
||||||
|
|
||||||
def cancel_order(self, order_param, asset_or_symbol=None):
|
|
||||||
order_id = order_param.id \
|
|
||||||
if isinstance(order_param, Order) else order_param
|
|
||||||
|
|
||||||
if asset_or_symbol is None and self.portfolio is not None:
|
|
||||||
asset_or_symbol = self._get_asset_from_order(order_id)
|
|
||||||
|
|
||||||
if asset_or_symbol is None:
|
|
||||||
log.debug(
|
|
||||||
'order not found in memory, cancelling order might fail '
|
|
||||||
'on some exchanges.'
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
symbol = self.get_symbol(asset_or_symbol) \
|
|
||||||
if asset_or_symbol is not None else None
|
|
||||||
self.api.cancel_order(id=order_id, symbol=symbol)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
|
||||||
def tickers(self, assets):
|
|
||||||
"""
|
|
||||||
Retrieve current tick data for the given assets
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
assets: list[TradingPair]
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list[dict[str, float]
|
|
||||||
|
|
||||||
"""
|
|
||||||
tickers = dict()
|
|
||||||
for asset in assets:
|
|
||||||
ccxt_symbol = self.get_symbol(asset)
|
|
||||||
ticker = self.api.fetch_ticker(ccxt_symbol)
|
|
||||||
|
|
||||||
ticker['last_traded'] = from_ms_timestamp(ticker['timestamp'])
|
|
||||||
|
|
||||||
if 'last_price' not in ticker:
|
|
||||||
# TODO: any more exceptions?
|
|
||||||
ticker['last_price'] = ticker['last']
|
|
||||||
|
|
||||||
# Using the volume represented in the base currency
|
|
||||||
ticker['volume'] = ticker['baseVolume'] \
|
|
||||||
if 'baseVolume' in ticker else 0
|
|
||||||
|
|
||||||
tickers[asset] = ticker
|
|
||||||
|
|
||||||
return tickers
|
|
||||||
|
|
||||||
def get_account(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_orderbook(self, asset, order_type='all', limit=None):
|
|
||||||
ccxt_symbol = self.get_symbol(asset)
|
|
||||||
|
|
||||||
params = dict()
|
|
||||||
if limit is not None:
|
|
||||||
params['depth'] = limit
|
|
||||||
|
|
||||||
order_book = self.api.fetch_order_book(ccxt_symbol, params)
|
|
||||||
|
|
||||||
order_types = ['bids', 'asks'] if order_type == 'all' else [order_type]
|
|
||||||
result = dict(last_traded=from_ms_timestamp(order_book['timestamp']))
|
|
||||||
for index, order_type in enumerate(order_types):
|
|
||||||
if limit is not None and index > limit - 1:
|
|
||||||
break
|
|
||||||
|
|
||||||
result[order_type] = []
|
|
||||||
for entry in order_book[order_type]:
|
|
||||||
result[order_type].append(dict(
|
|
||||||
rate=float(entry[0]),
|
|
||||||
quantity=float(entry[1])
|
|
||||||
))
|
|
||||||
|
|
||||||
return result
|
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#
|
||||||
|
# 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 time import sleep
|
||||||
|
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst.data.data_portal import DataPortal
|
||||||
|
from catalyst.exchange.exchange_errors import (
|
||||||
|
ExchangeRequestError,
|
||||||
|
ExchangeBarDataError
|
||||||
|
)
|
||||||
|
|
||||||
|
log = Logger('DataPortalExchange')
|
||||||
|
|
||||||
|
|
||||||
|
class DataPortalExchange(DataPortal):
|
||||||
|
def __init__(self, exchange, *args, **kwargs):
|
||||||
|
self.exchange = exchange
|
||||||
|
|
||||||
|
# TODO: put somewhere accessible by each algo
|
||||||
|
self.retry_get_history_window = 5
|
||||||
|
self.retry_get_spot_value = 5
|
||||||
|
self.retry_delay = 5
|
||||||
|
|
||||||
|
super(DataPortalExchange, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _get_history_window(self,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill=True,
|
||||||
|
attempt_index=0):
|
||||||
|
try:
|
||||||
|
return self.exchange.get_history_window(
|
||||||
|
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,
|
||||||
|
ffill=True):
|
||||||
|
return self._get_history_window(assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency,
|
||||||
|
ffill)
|
||||||
|
|
||||||
|
def _get_spot_value(self, assets, field, dt, data_frequency,
|
||||||
|
attempt_index=0):
|
||||||
|
try:
|
||||||
|
return self.exchange.get_spot_value(assets, field, dt,
|
||||||
|
data_frequency)
|
||||||
|
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):
|
||||||
|
return self._get_spot_value(assets, field, dt, data_frequency)
|
||||||
|
|
||||||
|
def get_adjusted_value(self, asset, field, dt,
|
||||||
|
perspective_dt,
|
||||||
|
data_frequency,
|
||||||
|
spot_value=None):
|
||||||
|
# TODO: does this pertain to cryptocurrencies?
|
||||||
|
raise NotImplementedError("get_adjusted_value is not implemented yet!")
|
||||||
+159
-652
File diff suppressed because it is too large
Load Diff
@@ -1,830 +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 pickle
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
from collections import deque
|
|
||||||
from datetime import timedelta
|
|
||||||
from os import listdir
|
|
||||||
from os.path import isfile, join
|
|
||||||
from time import sleep
|
|
||||||
|
|
||||||
import logbook
|
|
||||||
import pandas as pd
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
|
|
||||||
import catalyst.protocol as zp
|
|
||||||
from catalyst.algorithm import TradingAlgorithm
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.errors import OrderInBeforeTradingStart
|
|
||||||
from catalyst.exchange.exchange_blotter import ExchangeBlotter
|
|
||||||
from catalyst.exchange.exchange_errors import (
|
|
||||||
ExchangeRequestError,
|
|
||||||
ExchangePortfolioDataError,
|
|
||||||
ExchangeTransactionError,
|
|
||||||
OrphanOrderError)
|
|
||||||
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
|
|
||||||
ExchangeLimitOrder, ExchangeStopOrder
|
|
||||||
from catalyst.exchange.exchange_utils import save_algo_object, get_algo_object, \
|
|
||||||
get_algo_folder, get_algo_df, \
|
|
||||||
save_algo_df
|
|
||||||
from catalyst.exchange.live_graph_clock import LiveGraphClock
|
|
||||||
from catalyst.exchange.simple_clock import SimpleClock
|
|
||||||
from catalyst.exchange.stats_utils import get_pretty_stats
|
|
||||||
from catalyst.finance.execution import MarketOrder
|
|
||||||
from catalyst.finance.performance.period import calc_period_stats
|
|
||||||
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
|
||||||
from catalyst.utils.api_support import (
|
|
||||||
api_method,
|
|
||||||
disallowed_in_before_trading_start)
|
|
||||||
from catalyst.utils.input_validation import error_keywords, ensure_upper_case, \
|
|
||||||
expect_types
|
|
||||||
from catalyst.utils.math_utils import round_nearest
|
|
||||||
from catalyst.utils.preprocess import preprocess
|
|
||||||
|
|
||||||
log = logbook.Logger('exchange_algorithm', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeAlgorithmExecutor(AlgorithmSimulator):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(self.__class__, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.exchanges = kwargs.pop('exchanges', None)
|
|
||||||
|
|
||||||
super(ExchangeTradingAlgorithmBase, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def round_order(self, amount, asset):
|
|
||||||
"""
|
|
||||||
We need fractions with cryptocurrencies
|
|
||||||
|
|
||||||
:param amount:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return round_nearest(amount, asset.min_trade_size)
|
|
||||||
|
|
||||||
@api_method
|
|
||||||
@preprocess(symbol_str=ensure_upper_case)
|
|
||||||
def symbol(self, symbol_str, exchange_name=None):
|
|
||||||
"""Lookup an Equity by its ticker symbol.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
symbol_str : str
|
|
||||||
The ticker symbol for the equity to lookup.
|
|
||||||
exchange_name: str
|
|
||||||
The name of the exchange containing the symbol
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
equity : Equity
|
|
||||||
The equity that held the ticker symbol on the current
|
|
||||||
symbol lookup date.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
SymbolNotFound
|
|
||||||
Raised when the symbols was not held on the current lookup date.
|
|
||||||
|
|
||||||
See Also
|
|
||||||
--------
|
|
||||||
:func:`catalyst.api.set_symbol_lookup_date`
|
|
||||||
"""
|
|
||||||
# If the user has not set the symbol lookup date,
|
|
||||||
# use the end_session as the date for sybmol->sid resolution.
|
|
||||||
|
|
||||||
_lookup_date = self._symbol_lookup_date \
|
|
||||||
if self._symbol_lookup_date is not None \
|
|
||||||
else self.sim_params.end_session
|
|
||||||
|
|
||||||
if exchange_name is None:
|
|
||||||
exchange = list(self.exchanges.values())[0]
|
|
||||||
else:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
|
|
||||||
data_frequency = self.data_frequency \
|
|
||||||
if self.sim_params.arena == 'backtest' else None
|
|
||||||
return self.asset_finder.lookup_symbol(
|
|
||||||
symbol=symbol_str,
|
|
||||||
exchange=exchange,
|
|
||||||
data_frequency=data_frequency,
|
|
||||||
as_of_date=_lookup_date
|
|
||||||
)
|
|
||||||
|
|
||||||
def prepare_period_stats(self, start_dt, end_dt):
|
|
||||||
"""
|
|
||||||
Creates a dictionary representing the state of the tracker.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
start_dt: datetime
|
|
||||||
end_dt: datetime
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
I rewrote this in an attempt to better control the stats.
|
|
||||||
I don't want things to happen magically through complex logic
|
|
||||||
pertaining to backtesting.
|
|
||||||
|
|
||||||
"""
|
|
||||||
tracker = self.perf_tracker
|
|
||||||
period = tracker.todays_performance
|
|
||||||
|
|
||||||
pos_stats = period.position_tracker.stats()
|
|
||||||
period_stats = calc_period_stats(pos_stats, period.ending_cash)
|
|
||||||
|
|
||||||
stats = dict(
|
|
||||||
period_start=tracker.period_start,
|
|
||||||
period_end=tracker.period_end,
|
|
||||||
capital_base=tracker.capital_base,
|
|
||||||
progress=tracker.progress,
|
|
||||||
ending_value=period.ending_value,
|
|
||||||
ending_exposure=period.ending_exposure,
|
|
||||||
capital_used=period.cash_flow,
|
|
||||||
starting_value=period.starting_value,
|
|
||||||
starting_exposure=period.starting_exposure,
|
|
||||||
starting_cash=period.starting_cash,
|
|
||||||
ending_cash=period.ending_cash,
|
|
||||||
portfolio_value=period.ending_cash + period.ending_value,
|
|
||||||
pnl=period.pnl,
|
|
||||||
returns=period.returns,
|
|
||||||
period_open=period.period_open,
|
|
||||||
period_close=period.period_close,
|
|
||||||
gross_leverage=period_stats.gross_leverage,
|
|
||||||
net_leverage=period_stats.net_leverage,
|
|
||||||
short_exposure=pos_stats.short_exposure,
|
|
||||||
long_exposure=pos_stats.long_exposure,
|
|
||||||
short_value=pos_stats.short_value,
|
|
||||||
long_value=pos_stats.long_value,
|
|
||||||
longs_count=pos_stats.longs_count,
|
|
||||||
shorts_count=pos_stats.shorts_count,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Merging cumulative risk
|
|
||||||
stats.update(tracker.cumulative_risk_metrics.to_dict())
|
|
||||||
|
|
||||||
# Merging latest recorded variables
|
|
||||||
stats.update(self.recorded_vars)
|
|
||||||
|
|
||||||
stats['positions'] = period.position_tracker.get_positions_list()
|
|
||||||
|
|
||||||
# we want the key to be absent, not just empty
|
|
||||||
# Only include transactions for given dt
|
|
||||||
stats['transactions'] = []
|
|
||||||
for date in period.processed_transactions:
|
|
||||||
if start_dt <= date < end_dt:
|
|
||||||
transactions = period.processed_transactions[date]
|
|
||||||
for t in transactions:
|
|
||||||
stats['transactions'].append(t.to_dict())
|
|
||||||
|
|
||||||
stats['orders'] = []
|
|
||||||
for date in period.orders_by_modified:
|
|
||||||
if start_dt <= date < end_dt:
|
|
||||||
orders = period.orders_by_modified[date]
|
|
||||||
for order in orders:
|
|
||||||
stats['orders'].append(orders[order].to_dict())
|
|
||||||
|
|
||||||
return stats
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeTradingAlgorithmBacktest(ExchangeTradingAlgorithmBase):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ExchangeTradingAlgorithmBacktest, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.frame_stats = list()
|
|
||||||
self.blotter = ExchangeBlotter(
|
|
||||||
data_frequency=self.data_frequency,
|
|
||||||
# Default to NeverCancel in catalyst
|
|
||||||
cancel_policy=self.cancel_policy,
|
|
||||||
)
|
|
||||||
log.info('initialized trading algorithm in backtest mode')
|
|
||||||
|
|
||||||
def _calculate_order(self, asset, amount,
|
|
||||||
limit_price=None, stop_price=None, style=None):
|
|
||||||
# Raises a ZiplineError if invalid parameters are detected.
|
|
||||||
self.validate_order_params(asset,
|
|
||||||
amount,
|
|
||||||
limit_price,
|
|
||||||
stop_price,
|
|
||||||
style)
|
|
||||||
|
|
||||||
# Convert deprecated limit_price and stop_price parameters to use
|
|
||||||
# ExecutionStyle objects.
|
|
||||||
style = self.__convert_order_params_for_blotter(limit_price,
|
|
||||||
stop_price,
|
|
||||||
style)
|
|
||||||
return amount, style
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
|
||||||
"""
|
|
||||||
Helper method for converting deprecated limit_price and stop_price
|
|
||||||
arguments into ExecutionStyle instances.
|
|
||||||
|
|
||||||
This function assumes that either style == None or (limit_price,
|
|
||||||
stop_price) == (None, None).
|
|
||||||
"""
|
|
||||||
if style:
|
|
||||||
assert (limit_price, stop_price) == (None, None)
|
|
||||||
return style
|
|
||||||
if limit_price and stop_price:
|
|
||||||
return ExchangeStopLimitOrder(limit_price, stop_price)
|
|
||||||
if limit_price:
|
|
||||||
return ExchangeLimitOrder(limit_price)
|
|
||||||
if stop_price:
|
|
||||||
return ExchangeStopOrder(stop_price)
|
|
||||||
else:
|
|
||||||
return MarketOrder()
|
|
||||||
|
|
||||||
def is_last_frame_of_day(self, data):
|
|
||||||
# TODO: adjust here to support more intervals
|
|
||||||
next_frame_dt = data.current_dt + timedelta(minutes=1)
|
|
||||||
if next_frame_dt.date() > data.current_dt.date():
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def handle_data(self, data):
|
|
||||||
super(ExchangeTradingAlgorithmBacktest, self).handle_data(data)
|
|
||||||
|
|
||||||
if self.data_frequency == 'minute':
|
|
||||||
frame_stats = self.prepare_period_stats(
|
|
||||||
data.current_dt, data.current_dt + timedelta(minutes=1)
|
|
||||||
)
|
|
||||||
self.frame_stats.append(frame_stats)
|
|
||||||
|
|
||||||
def _create_stats_df(self):
|
|
||||||
stats = pd.DataFrame(self.frame_stats)
|
|
||||||
stats.set_index('period_close', inplace=True, drop=False)
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def analyze(self, perf):
|
|
||||||
stats = self._create_stats_df() if self.data_frequency == 'minute' \
|
|
||||||
else perf
|
|
||||||
super(ExchangeTradingAlgorithmBacktest, self).analyze(stats)
|
|
||||||
|
|
||||||
def run(self, data=None, overwrite_sim_params=True):
|
|
||||||
perf = super(ExchangeTradingAlgorithmBacktest, self).run(
|
|
||||||
data, overwrite_sim_params
|
|
||||||
)
|
|
||||||
# Rebuilding the stats to support minute data
|
|
||||||
stats = self._create_stats_df() if self.data_frequency == 'minute' \
|
|
||||||
else perf
|
|
||||||
return stats
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.algo_namespace = kwargs.pop('algo_namespace', None)
|
|
||||||
self.live_graph = kwargs.pop('live_graph', None)
|
|
||||||
self.simulate_orders = kwargs.pop('simulate_orders', None)
|
|
||||||
|
|
||||||
self._clock = None
|
|
||||||
self.frame_stats = deque(maxlen=60)
|
|
||||||
|
|
||||||
self.pnl_stats = get_algo_df(self.algo_namespace, 'pnl_stats')
|
|
||||||
|
|
||||||
self.custom_signals_stats = \
|
|
||||||
get_algo_df(self.algo_namespace, 'custom_signals_stats')
|
|
||||||
|
|
||||||
self.exposure_stats = \
|
|
||||||
get_algo_df(self.algo_namespace, 'exposure_stats')
|
|
||||||
|
|
||||||
self.is_running = True
|
|
||||||
|
|
||||||
self.retry_check_open_orders = 5
|
|
||||||
self.retry_synchronize_portfolio = 5
|
|
||||||
self.retry_get_open_orders = 5
|
|
||||||
self.retry_order = 2
|
|
||||||
self.retry_delay = 5
|
|
||||||
|
|
||||||
self.stats_minutes = 5
|
|
||||||
|
|
||||||
super(ExchangeTradingAlgorithmLive, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
|
||||||
|
|
||||||
log.info('initialized trading algorithm in live mode')
|
|
||||||
|
|
||||||
def signal_handler(self, signal, frame):
|
|
||||||
"""
|
|
||||||
Handles the keyboard interruption signal.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
signal
|
|
||||||
frame
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.is_running = False
|
|
||||||
|
|
||||||
if self._analyze is None:
|
|
||||||
log.info('Interruption signal detected {}, exiting the '
|
|
||||||
'algorithm'.format(signal))
|
|
||||||
|
|
||||||
else:
|
|
||||||
log.info('Interruption signal detected {}, calling `analyze()` '
|
|
||||||
'before exiting the algorithm'.format(signal))
|
|
||||||
|
|
||||||
algo_folder = get_algo_folder(self.algo_namespace)
|
|
||||||
folder = join(algo_folder, 'daily_perf')
|
|
||||||
files = [f for f in listdir(folder) if isfile(join(folder, f))]
|
|
||||||
|
|
||||||
daily_perf_list = []
|
|
||||||
for item in files:
|
|
||||||
filename = join(folder, item)
|
|
||||||
with open(filename, 'rb') as handle:
|
|
||||||
daily_perf_list.append(pickle.load(handle))
|
|
||||||
|
|
||||||
stats = pd.DataFrame(daily_perf_list)
|
|
||||||
|
|
||||||
self.analyze(stats)
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def clock(self):
|
|
||||||
if self._clock is None:
|
|
||||||
return self._create_clock()
|
|
||||||
else:
|
|
||||||
return self._clock
|
|
||||||
|
|
||||||
def _create_clock(self):
|
|
||||||
|
|
||||||
# The calendar's execution times are the minutes over which we actually
|
|
||||||
# want to run the clock. Typically the execution times simply adhere to
|
|
||||||
# the market open and close times. In the case of the futures calendar,
|
|
||||||
# for example, we only want to simulate over a subset of the full 24
|
|
||||||
# hour calendar, so the execution times dictate a market open time of
|
|
||||||
# 6:31am US/Eastern and a close of 5:00pm US/Eastern.
|
|
||||||
|
|
||||||
# In our case, we are trading around the clock, so the market close
|
|
||||||
# corresponds to the last minute of the day.
|
|
||||||
|
|
||||||
# This method is taken from TradingAlgorithm.
|
|
||||||
# The clock has been replaced to use RealtimeClock
|
|
||||||
# TODO: should we apply a time skew? not sure to understand the utility.
|
|
||||||
|
|
||||||
log.debug('creating clock')
|
|
||||||
if self.live_graph:
|
|
||||||
self._clock = LiveGraphClock(
|
|
||||||
self.sim_params.sessions,
|
|
||||||
context=self
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._clock = SimpleClock(
|
|
||||||
self.sim_params.sessions,
|
|
||||||
)
|
|
||||||
|
|
||||||
return self._clock
|
|
||||||
|
|
||||||
def _create_generator(self, sim_params):
|
|
||||||
if self.perf_tracker is None:
|
|
||||||
self.perf_tracker = get_algo_object(
|
|
||||||
algo_name=self.algo_namespace,
|
|
||||||
key='perf_tracker'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Call the simulation trading algorithm for side-effects:
|
|
||||||
# it creates the perf tracker
|
|
||||||
TradingAlgorithm._create_generator(self, sim_params)
|
|
||||||
self.trading_client = ExchangeAlgorithmExecutor(
|
|
||||||
self,
|
|
||||||
sim_params,
|
|
||||||
self.data_portal,
|
|
||||||
self.clock,
|
|
||||||
self._create_benchmark_source(),
|
|
||||||
self.restrictions,
|
|
||||||
universe_func=self._calculate_universe
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.trading_client.transform()
|
|
||||||
|
|
||||||
def updated_portfolio(self):
|
|
||||||
"""
|
|
||||||
We skip the entire performance tracker business and update the
|
|
||||||
portfolio directly.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
ExchangePortfolio
|
|
||||||
|
|
||||||
"""
|
|
||||||
# TODO: build cumulative portfolio
|
|
||||||
return self.perf_tracker.get_portfolio(False)
|
|
||||||
|
|
||||||
def updated_account(self):
|
|
||||||
return self.perf_tracker.get_account(False)
|
|
||||||
|
|
||||||
def _synchronize_portfolio(self, attempt_index=0):
|
|
||||||
try:
|
|
||||||
for exchange_name in self.exchanges:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
|
|
||||||
exchange.synchronize_portfolio()
|
|
||||||
|
|
||||||
# Applying the updated last_sales_price to the positions
|
|
||||||
# in the performance tracker. This seems a bit redundant
|
|
||||||
# but it will make sense when we have multiple exchange portfolios
|
|
||||||
# feeding into the same performance tracker.
|
|
||||||
tracker = self.perf_tracker.todays_performance.position_tracker
|
|
||||||
for asset in exchange.portfolio.positions:
|
|
||||||
position = exchange.portfolio.positions[asset]
|
|
||||||
tracker.update_position(
|
|
||||||
asset=asset,
|
|
||||||
last_sale_date=position.last_sale_date,
|
|
||||||
last_sale_price=position.last_sale_price
|
|
||||||
)
|
|
||||||
except ExchangeRequestError as e:
|
|
||||||
log.warn(
|
|
||||||
'update portfolio attempt {}: {}'.format(attempt_index, e)
|
|
||||||
)
|
|
||||||
if attempt_index < self.retry_synchronize_portfolio:
|
|
||||||
sleep(self.retry_delay)
|
|
||||||
self._synchronize_portfolio(attempt_index + 1)
|
|
||||||
else:
|
|
||||||
raise ExchangePortfolioDataError(
|
|
||||||
data_type='update-portfolio',
|
|
||||||
attempts=attempt_index,
|
|
||||||
error=e
|
|
||||||
)
|
|
||||||
|
|
||||||
def _check_open_orders(self, attempt_index=0):
|
|
||||||
try:
|
|
||||||
orders = list()
|
|
||||||
for exchange_name in self.exchanges:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
exchange_orders = exchange.check_open_orders()
|
|
||||||
|
|
||||||
orders += exchange_orders
|
|
||||||
|
|
||||||
return orders
|
|
||||||
except ExchangeRequestError as e:
|
|
||||||
log.warn(
|
|
||||||
'check open orders attempt {}: {}'.format(attempt_index, e)
|
|
||||||
)
|
|
||||||
if attempt_index < self.retry_check_open_orders:
|
|
||||||
sleep(self.retry_delay)
|
|
||||||
return self._check_open_orders(attempt_index + 1)
|
|
||||||
else:
|
|
||||||
raise ExchangePortfolioDataError(
|
|
||||||
data_type='order-status',
|
|
||||||
attempts=attempt_index,
|
|
||||||
error=e
|
|
||||||
)
|
|
||||||
|
|
||||||
def add_pnl_stats(self, period_stats):
|
|
||||||
"""
|
|
||||||
Save p&l stats.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
period_stats
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
starting = period_stats['starting_cash']
|
|
||||||
current = period_stats['portfolio_value']
|
|
||||||
appreciation = (current / starting) - 1
|
|
||||||
perc = (appreciation * 100) if current != 0 else 0
|
|
||||||
|
|
||||||
log.debug('adding pnl stats: {:6f}%'.format(perc))
|
|
||||||
|
|
||||||
df = pd.DataFrame(
|
|
||||||
data=[dict(performance=perc)],
|
|
||||||
index=[period_stats['period_close']]
|
|
||||||
)
|
|
||||||
self.pnl_stats = pd.concat([self.pnl_stats, df])
|
|
||||||
|
|
||||||
save_algo_df(self.algo_namespace, 'pnl_stats', self.pnl_stats)
|
|
||||||
|
|
||||||
def add_custom_signals_stats(self, period_stats):
|
|
||||||
"""
|
|
||||||
Save custom signals stats.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
period_stats
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
log.debug('adding custom signals stats: {}'.format(self.recorded_vars))
|
|
||||||
df = pd.DataFrame(
|
|
||||||
data=[self.recorded_vars],
|
|
||||||
index=[period_stats['period_close']],
|
|
||||||
)
|
|
||||||
self.custom_signals_stats = pd.concat([self.custom_signals_stats, df])
|
|
||||||
|
|
||||||
save_algo_df(self.algo_namespace, 'custom_signals_stats',
|
|
||||||
self.custom_signals_stats)
|
|
||||||
|
|
||||||
def add_exposure_stats(self, period_stats):
|
|
||||||
"""
|
|
||||||
Save exposure stats.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
period_stats
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
data = dict(
|
|
||||||
long_exposure=period_stats['long_exposure'],
|
|
||||||
base_currency=period_stats['ending_cash']
|
|
||||||
)
|
|
||||||
log.debug('adding exposure stats: {}'.format(data))
|
|
||||||
|
|
||||||
df = pd.DataFrame(
|
|
||||||
data=[data],
|
|
||||||
index=[period_stats['period_close']],
|
|
||||||
)
|
|
||||||
self.exposure_stats = pd.concat([self.exposure_stats, df])
|
|
||||||
|
|
||||||
save_algo_df(
|
|
||||||
self.algo_namespace, 'exposure_stats', self.exposure_stats
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_data(self, data):
|
|
||||||
"""
|
|
||||||
Wrapper around the handle_data method of each algo.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
data
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not self.is_running:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._synchronize_portfolio()
|
|
||||||
|
|
||||||
transactions = self._check_open_orders()
|
|
||||||
if len(transactions) > 0:
|
|
||||||
for transaction in transactions:
|
|
||||||
self.perf_tracker.process_transaction(transaction)
|
|
||||||
|
|
||||||
self.perf_tracker.update_performance()
|
|
||||||
|
|
||||||
if self._handle_data:
|
|
||||||
self._handle_data(self, data)
|
|
||||||
|
|
||||||
# Unlike trading controls which remain constant unless placing an
|
|
||||||
# order, account controls can change each bar. Thus, must check
|
|
||||||
# every bar no matter if the algorithm places an order or not.
|
|
||||||
self.validate_account_controls()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Since the clock runs 24/7, I trying to disable the daily
|
|
||||||
# Performance tracker and keep only minute and cumulative
|
|
||||||
self.perf_tracker.update_performance()
|
|
||||||
|
|
||||||
frame_stats = self.prepare_period_stats(
|
|
||||||
data.current_dt, data.current_dt + timedelta(minutes=1))
|
|
||||||
|
|
||||||
# Saving the last hour in memory
|
|
||||||
self.frame_stats.append(frame_stats)
|
|
||||||
|
|
||||||
self.add_pnl_stats(frame_stats)
|
|
||||||
if self.recorded_vars:
|
|
||||||
self.add_custom_signals_stats(frame_stats)
|
|
||||||
recorded_cols = list(self.recorded_vars.keys())
|
|
||||||
else:
|
|
||||||
recorded_cols = None
|
|
||||||
|
|
||||||
self.add_exposure_stats(frame_stats)
|
|
||||||
|
|
||||||
print_df = pd.DataFrame(list(self.frame_stats))
|
|
||||||
log.info(
|
|
||||||
'statistics for the last {stats_minutes} minutes:\n{stats}'.format(
|
|
||||||
stats_minutes=self.stats_minutes,
|
|
||||||
stats=get_pretty_stats(
|
|
||||||
stats_df=print_df,
|
|
||||||
recorded_cols=recorded_cols,
|
|
||||||
num_rows=self.stats_minutes
|
|
||||||
)
|
|
||||||
))
|
|
||||||
|
|
||||||
today = pd.to_datetime('today', utc=True)
|
|
||||||
daily_stats = self.prepare_period_stats(
|
|
||||||
start_dt=today,
|
|
||||||
end_dt=pd.Timestamp.utcnow()
|
|
||||||
)
|
|
||||||
save_algo_object(
|
|
||||||
algo_name=self.algo_namespace,
|
|
||||||
key=today.strftime('%Y-%m-%d'),
|
|
||||||
obj=daily_stats,
|
|
||||||
rel_path='daily_perf'
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.warn('unable to calculate performance: {}'.format(e))
|
|
||||||
|
|
||||||
# TODO: pickle does not seem to work in python 3
|
|
||||||
try:
|
|
||||||
save_algo_object(
|
|
||||||
algo_name=self.algo_namespace,
|
|
||||||
key='perf_tracker',
|
|
||||||
obj=self.perf_tracker
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log.warn('unable to save minute perfs to disk: {}'.format(e))
|
|
||||||
|
|
||||||
try:
|
|
||||||
for exchange_name in self.exchanges:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
save_algo_object(
|
|
||||||
algo_name=self.algo_namespace,
|
|
||||||
key='portfolio_{}'.format(exchange_name),
|
|
||||||
obj=exchange.portfolio
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log.warn('unable to save portfolio to disk: {}'.format(e))
|
|
||||||
|
|
||||||
def _order(self,
|
|
||||||
asset,
|
|
||||||
amount,
|
|
||||||
limit_price=None,
|
|
||||||
stop_price=None,
|
|
||||||
style=None,
|
|
||||||
attempt_index=0):
|
|
||||||
try:
|
|
||||||
exchange = self.exchanges[asset.exchange]
|
|
||||||
return exchange.order(asset, amount, limit_price,
|
|
||||||
stop_price,
|
|
||||||
style)
|
|
||||||
except ExchangeRequestError as e:
|
|
||||||
log.warn(
|
|
||||||
'order attempt {}: {}'.format(attempt_index, e)
|
|
||||||
)
|
|
||||||
if attempt_index < self.retry_order:
|
|
||||||
sleep(self.retry_delay)
|
|
||||||
return self._order(
|
|
||||||
asset, amount, limit_price, stop_price, style,
|
|
||||||
attempt_index + 1)
|
|
||||||
else:
|
|
||||||
raise ExchangeTransactionError(
|
|
||||||
transaction_type='order',
|
|
||||||
attempts=attempt_index,
|
|
||||||
error=e
|
|
||||||
)
|
|
||||||
|
|
||||||
@api_method
|
|
||||||
@disallowed_in_before_trading_start(OrderInBeforeTradingStart())
|
|
||||||
@expect_types(asset=TradingPair)
|
|
||||||
def order(self,
|
|
||||||
asset,
|
|
||||||
amount,
|
|
||||||
limit_price=None,
|
|
||||||
stop_price=None,
|
|
||||||
style=None):
|
|
||||||
"""
|
|
||||||
We use the exchange specific portfolio to place orders.
|
|
||||||
The cumulative portfolio does not contain open orders but exchange
|
|
||||||
portfolios do.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
asset: TradingPair
|
|
||||||
amount: float
|
|
||||||
limit_price: float
|
|
||||||
stop_price: float
|
|
||||||
style: Style
|
|
||||||
order: Order
|
|
||||||
The catalyst order object or None
|
|
||||||
"""
|
|
||||||
amount, style = self._calculate_order(asset, amount,
|
|
||||||
limit_price, stop_price,
|
|
||||||
style)
|
|
||||||
|
|
||||||
order_id = self._order(asset, amount, limit_price, stop_price, style)
|
|
||||||
|
|
||||||
exchange = self.exchanges[asset.exchange]
|
|
||||||
exchange_portfolio = exchange.portfolio
|
|
||||||
if order_id is not None:
|
|
||||||
|
|
||||||
if order_id in exchange_portfolio.open_orders:
|
|
||||||
order = exchange_portfolio.open_orders[order_id]
|
|
||||||
self.perf_tracker.process_order(order)
|
|
||||||
return order
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise OrphanOrderError(
|
|
||||||
order_id=order_id,
|
|
||||||
exchange=exchange.name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.warn('unable to order {} {} on exchange {}'.format(
|
|
||||||
amount, asset.symbol, asset.exchange))
|
|
||||||
return None
|
|
||||||
|
|
||||||
@api_method
|
|
||||||
def batch_market_order(self, share_counts):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def _get_open_orders(self, asset=None, attempt_index=0):
|
|
||||||
try:
|
|
||||||
if asset:
|
|
||||||
exchange = self.exchanges[asset.exchange]
|
|
||||||
return exchange.get_open_orders(asset)
|
|
||||||
|
|
||||||
else:
|
|
||||||
open_orders = []
|
|
||||||
for exchange_name in self.exchanges:
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
exchange_orders = exchange.get_open_orders()
|
|
||||||
open_orders.append(exchange_orders)
|
|
||||||
|
|
||||||
return open_orders
|
|
||||||
except ExchangeRequestError as e:
|
|
||||||
log.warn(
|
|
||||||
'open orders attempt {}: {}'.format(attempt_index, e)
|
|
||||||
)
|
|
||||||
if attempt_index < self.retry_get_open_orders:
|
|
||||||
sleep(self.retry_delay)
|
|
||||||
return self._get_open_orders(asset, attempt_index + 1)
|
|
||||||
else:
|
|
||||||
raise ExchangePortfolioDataError(
|
|
||||||
data_type='open-orders',
|
|
||||||
attempts=attempt_index,
|
|
||||||
error=e
|
|
||||||
)
|
|
||||||
|
|
||||||
@error_keywords(sid='Keyword argument `sid` is no longer supported for '
|
|
||||||
'get_open_orders. Use `asset` instead.')
|
|
||||||
@api_method
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
return self._get_open_orders(asset)
|
|
||||||
|
|
||||||
@api_method
|
|
||||||
def get_order(self, order_id, exchange_name):
|
|
||||||
"""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.
|
|
||||||
execution_price: float
|
|
||||||
The execution price per share of the order
|
|
||||||
"""
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
return exchange.get_order(order_id)
|
|
||||||
|
|
||||||
@api_method
|
|
||||||
def cancel_order(self, order_param, exchange_name):
|
|
||||||
"""Cancel an open order.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
order_param : str or Order
|
|
||||||
The order_id or order object to cancel.
|
|
||||||
"""
|
|
||||||
exchange = self.exchanges[exchange_name]
|
|
||||||
|
|
||||||
order_id = order_param
|
|
||||||
if isinstance(order_param, zp.Order):
|
|
||||||
order_id = order_param.id
|
|
||||||
|
|
||||||
exchange.cancel_order(order_id)
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
|
|
||||||
from catalyst import get_calendar
|
|
||||||
from catalyst.data.minute_bars import BcolzMinuteBarReader, \
|
|
||||||
BcolzMinuteBarWriter
|
|
||||||
|
|
||||||
|
|
||||||
class BcolzExchangeBarWriter(BcolzMinuteBarWriter):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._data_frequency = kwargs.pop('data_frequency', None)
|
|
||||||
kwargs.pop('minutes_per_day', None)
|
|
||||||
kwargs.pop('calendar', None)
|
|
||||||
|
|
||||||
end_session = kwargs.pop('end_session', None)
|
|
||||||
if end_session is not None:
|
|
||||||
end_session = end_session.floor('1d')
|
|
||||||
|
|
||||||
minutes_per_day = 1440 if self._data_frequency == 'minute' else 1
|
|
||||||
default_ohlc_ratio = kwargs.pop('default_ohlc_ratio', 100000000)
|
|
||||||
calendar = get_calendar('OPEN')
|
|
||||||
|
|
||||||
super(BcolzExchangeBarWriter, self) \
|
|
||||||
.__init__(*args, **dict(kwargs,
|
|
||||||
minutes_per_day=minutes_per_day,
|
|
||||||
default_ohlc_ratio=default_ohlc_ratio,
|
|
||||||
calendar=calendar,
|
|
||||||
end_session=end_session
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
class BcolzExchangeBarReader(BcolzMinuteBarReader):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self._data_frequency = kwargs.pop('data_frequency', None)
|
|
||||||
|
|
||||||
super(BcolzExchangeBarReader, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def data_frequency(self):
|
|
||||||
return self._data_frequency
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list of np.ndarray
|
|
||||||
A list with an entry per field of ndarrays with shape
|
|
||||||
(minutes in range, sids) with a dtype of float64, containing the
|
|
||||||
values for the respective field over start and end dt range.
|
|
||||||
"""
|
|
||||||
start_idx = self._find_position_of_minute(start_dt)
|
|
||||||
end_idx = self._find_position_of_minute(end_dt)
|
|
||||||
|
|
||||||
periods = self.calendar.minutes_in_range(start_dt, end_dt) \
|
|
||||||
if self.data_frequency == 'minute' \
|
|
||||||
else self.calendar.sessions_in_range(start_dt, end_dt)
|
|
||||||
|
|
||||||
num_days = len(periods)
|
|
||||||
shape = num_days, len(sids)
|
|
||||||
|
|
||||||
all_fields = fields[:]
|
|
||||||
if len(all_fields) == 1 and all_fields[0] == 'volume':
|
|
||||||
all_fields.insert(0, 'close')
|
|
||||||
|
|
||||||
mask = None
|
|
||||||
data = []
|
|
||||||
for field in all_fields:
|
|
||||||
if field != 'volume':
|
|
||||||
out = np.full(shape, np.nan)
|
|
||||||
else:
|
|
||||||
out = np.zeros(shape, dtype=np.float64)
|
|
||||||
|
|
||||||
for i, sid in enumerate(sids):
|
|
||||||
carray = self._open_minute_file(field, sid)
|
|
||||||
a = carray[start_idx:end_idx + 1]
|
|
||||||
|
|
||||||
if mask is None:
|
|
||||||
mask = a != 0
|
|
||||||
|
|
||||||
inverse_ratio = self._ohlc_ratio_inverse_for_sid(sid)
|
|
||||||
out[:len(mask), i][mask] = (
|
|
||||||
a[mask] * inverse_ratio
|
|
||||||
)
|
|
||||||
|
|
||||||
if field in fields:
|
|
||||||
data.append(out)
|
|
||||||
|
|
||||||
return data
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.finance.blotter import Blotter
|
|
||||||
from catalyst.finance.commission import CommissionModel
|
|
||||||
from catalyst.finance.slippage import SlippageModel
|
|
||||||
from catalyst.finance.transaction import create_transaction
|
|
||||||
|
|
||||||
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: should work with set_commission and set_slippage
|
|
||||||
DEFAULT_SLIPPAGE_SPREAD = 0.0001
|
|
||||||
DEFAULT_MAKER_FEE = 0.0015
|
|
||||||
DEFAULT_TAKER_FEE = 0.0025
|
|
||||||
|
|
||||||
|
|
||||||
class TradingPairFeeSchedule(CommissionModel):
|
|
||||||
"""
|
|
||||||
Calculates a commission for a transaction based on a per percentage fee.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
fee : float, optional
|
|
||||||
The percentage fee.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
maker_fee=DEFAULT_MAKER_FEE,
|
|
||||||
taker_fee=DEFAULT_TAKER_FEE):
|
|
||||||
self.maker_fee = maker_fee
|
|
||||||
self.taker_fee = taker_fee
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return (
|
|
||||||
'{class_name}(maker_fee={maker_fee}, '
|
|
||||||
'taker_fee={taker_fee})'.format(
|
|
||||||
class_name=self.__class__.__name__,
|
|
||||||
maker_fee=self.maker_fee,
|
|
||||||
taker_fee=self.taker_fee,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def calculate(self, order, transaction):
|
|
||||||
"""
|
|
||||||
Calculate the final fee based on the order parameters.
|
|
||||||
|
|
||||||
:param order:
|
|
||||||
:param transaction:
|
|
||||||
|
|
||||||
:return float:
|
|
||||||
The total commission.
|
|
||||||
"""
|
|
||||||
cost = abs(transaction.amount) * transaction.price
|
|
||||||
|
|
||||||
# Assuming just the taker fee for now
|
|
||||||
fee = cost * self.taker_fee
|
|
||||||
return fee
|
|
||||||
|
|
||||||
|
|
||||||
class TradingPairFixedSlippage(SlippageModel):
|
|
||||||
"""
|
|
||||||
Model slippage as a fixed spread.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
spread : float, optional
|
|
||||||
spread / 2 will be added to buys and subtracted from sells.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, spread=DEFAULT_SLIPPAGE_SPREAD):
|
|
||||||
super(TradingPairFixedSlippage, self).__init__()
|
|
||||||
self.spread = spread
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '{class_name}(spread={spread})'.format(
|
|
||||||
class_name=self.__class__.__name__, spread=self.spread,
|
|
||||||
)
|
|
||||||
|
|
||||||
def simulate(self, data, asset, orders_for_asset):
|
|
||||||
self._volume_for_bar = 0
|
|
||||||
|
|
||||||
price = data.current(asset, 'close')
|
|
||||||
|
|
||||||
dt = data.current_dt
|
|
||||||
for order in orders_for_asset:
|
|
||||||
if order.open_amount == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
order.check_triggers(price, dt)
|
|
||||||
if not order.triggered:
|
|
||||||
log.debug('order has not reached the trigger at current '
|
|
||||||
'price {}'.format(price))
|
|
||||||
continue
|
|
||||||
|
|
||||||
execution_price, execution_volume = self.process_order(data, order)
|
|
||||||
|
|
||||||
transaction = create_transaction(
|
|
||||||
order, dt, execution_price, execution_volume
|
|
||||||
)
|
|
||||||
|
|
||||||
self._volume_for_bar += abs(transaction.amount)
|
|
||||||
yield order, transaction
|
|
||||||
|
|
||||||
def process_order(self, data, order):
|
|
||||||
price = data.current(order.asset, 'close')
|
|
||||||
|
|
||||||
if order.amount > 0:
|
|
||||||
# Buy order
|
|
||||||
adj_price = price * (1 + self.spread)
|
|
||||||
else:
|
|
||||||
# Sell order
|
|
||||||
adj_price = price * (1 - self.spread)
|
|
||||||
|
|
||||||
log.debug('added slippage to price: {} => {}'.format(price, adj_price))
|
|
||||||
|
|
||||||
return adj_price, order.amount
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeBlotter(Blotter):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ExchangeBlotter, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
# Using the equity models for now
|
|
||||||
# We may be able to define more sophisticated models based on the fee
|
|
||||||
# structure of each exchange.
|
|
||||||
self.slippage_models = {
|
|
||||||
TradingPair: TradingPairFixedSlippage()
|
|
||||||
}
|
|
||||||
self.commission_models = {
|
|
||||||
TradingPair: TradingPairFeeSchedule()
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -16,16 +16,16 @@ from time import sleep
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
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
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = Logger('ExchangeClock')
|
||||||
|
|
||||||
log = Logger('ExchangeClock', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleClock(object):
|
class ExchangeClock(object):
|
||||||
"""Realtime clock for live trading.
|
"""Realtime clock for live trading.
|
||||||
|
|
||||||
This class is a drop-in replacement for
|
This class is a drop-in replacement for
|
||||||
@@ -1,406 +0,0 @@
|
|||||||
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.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,
|
|
||||||
ExchangeBarDataError,
|
|
||||||
PricingDataNotLoadedError)
|
|
||||||
from catalyst.exchange.exchange_utils import get_frequency, resample_history_df
|
|
||||||
|
|
||||||
log = Logger('DataPortalExchange', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class DataPortalExchangeBase(DataPortal):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
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)
|
|
||||||
|
|
||||||
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_name,
|
|
||||||
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):
|
|
||||||
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
|
|
||||||
|
|
||||||
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_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,
|
|
||||||
ffill)
|
|
||||||
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
|
|
||||||
|
|
||||||
"""
|
|
||||||
bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
|
|
||||||
|
|
||||||
freq, candle_size, unit, adj_data_frequency = get_frequency(
|
|
||||||
frequency, data_frequency
|
|
||||||
)
|
|
||||||
adj_bar_count = candle_size * bar_count
|
|
||||||
trailing_bar_count = candle_size - 1
|
|
||||||
|
|
||||||
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,
|
|
||||||
trailing_bar_count=trailing_bar_count
|
|
||||||
)
|
|
||||||
|
|
||||||
df = resample_history_df(pd.DataFrame(series), freq, field)
|
|
||||||
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,24 +1,6 @@
|
|||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from catalyst.errors import ZiplineError
|
from catalyst.errors import ZiplineError
|
||||||
|
|
||||||
|
|
||||||
def silent_except_hook(exctype, excvalue, exctraceback):
|
|
||||||
if exctype in [PricingDataBeforeTradingError, PricingDataNotLoadedError,
|
|
||||||
SymbolNotFoundOnExchange, NoDataAvailableOnExchange,
|
|
||||||
ExchangeAuthEmpty]:
|
|
||||||
fn = traceback.extract_tb(exctraceback)[-1][0]
|
|
||||||
ln = traceback.extract_tb(exctraceback)[-1][1]
|
|
||||||
print("Error traceback: {1} (line {2})\n"
|
|
||||||
"{0.__name__}: {3}".format(exctype, fn, ln, excvalue))
|
|
||||||
else:
|
|
||||||
sys.__excepthook__(exctype, excvalue, exctraceback)
|
|
||||||
|
|
||||||
|
|
||||||
sys.excepthook = silent_except_hook
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeRequestError(ZiplineError):
|
class ExchangeRequestError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Request failed: {error}'
|
'Request failed: {error}'
|
||||||
@@ -52,13 +34,6 @@ class ExchangeTransactionError(ZiplineError):
|
|||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class ExchangeNotFoundError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Exchange {exchange_name} not found. Please specify exchanges '
|
|
||||||
'supported by Catalyst and verify spelling for accuracy.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeAuthNotFound(ZiplineError):
|
class ExchangeAuthNotFound(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Please create an auth.json file containing the api token and key for '
|
'Please create an auth.json file containing the api token and key for '
|
||||||
@@ -66,13 +41,6 @@ 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 '
|
||||||
@@ -86,161 +54,7 @@ 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.'
|
'History frequency {frequency} not supported by the exchange.'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class MismatchingFrequencyError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Bar aggregate frequency {frequency} not compatible with '
|
|
||||||
'data frequency {data_frequency}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidSymbolError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Invalid trading pair symbol: {symbol}. '
|
|
||||||
'Catalyst symbols must follow this convention: '
|
|
||||||
'[Market Currency]_[Base Currency]. For example: eth_usd, btc_usd, '
|
|
||||||
'neo_eth, ubq_btc. Error details: {error}'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidOrderStyle(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Order style {style} not supported by exchange {exchange}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class CreateOrderError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Unable to create order on exchange {exchange} {error}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class OrderNotFound(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Order {order_id} not found on exchange {exchange}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class OrphanOrderError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Order {order_id} found in exchange {exchange} but not tracked by '
|
|
||||||
'the algorithm.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class OrphanOrderReverseError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Order {order_id} tracked by algorithm, but not found in exchange {exchange}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class OrderCancelError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Unable to cancel order {order_id} on exchange {exchange} {error}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class SidHashError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Unable to hash sid from symbol {symbol}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class BaseCurrencyNotFoundError(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Algorithm base currency {base_currency} not found in exchange '
|
|
||||||
'{exchange}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class MismatchingBaseCurrencies(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Unable to trade with base currency {base_currency} when the '
|
|
||||||
'algorithm uses {algo_currency}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class MismatchingBaseCurrenciesExchanges(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Unable to trade with base currency {base_currency} when the '
|
|
||||||
'exchange {exchange_name} users {exchange_currency}.'
|
|
||||||
).strip()
|
|
||||||
|
|
||||||
|
|
||||||
class SymbolNotFoundOnExchange(ZiplineError):
|
|
||||||
"""
|
|
||||||
Raised when a symbol() call contains a non-existent symbol.
|
|
||||||
"""
|
|
||||||
msg = ('Symbol {symbol} not found on exchange {exchange}. '
|
|
||||||
'Choose from: {supported_symbols}').strip()
|
|
||||||
|
|
||||||
|
|
||||||
class BundleNotFoundError(ZiplineError):
|
|
||||||
msg = ('Unable to find bundle data for exchange {exchange} and '
|
|
||||||
'data frequency {data_frequency}.'
|
|
||||||
'Please ingest some price data.'
|
|
||||||
'See `catalyst ingest-exchange --help` for details.').strip()
|
|
||||||
|
|
||||||
|
|
||||||
class TempBundleNotFoundError(ZiplineError):
|
|
||||||
msg = ('Temporary bundle not found in: {path}.').strip()
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyValuesInBundleError(ZiplineError):
|
|
||||||
msg = ('{name} with end minute {end_minute} has empty rows '
|
|
||||||
'in ranges: {dates}').strip()
|
|
||||||
|
|
||||||
|
|
||||||
class PricingDataBeforeTradingError(ZiplineError):
|
|
||||||
msg = ('Pricing data for trading pairs {symbols} on exchange {exchange} '
|
|
||||||
'starts on {first_trading_day}, but you are either trying to trade or '
|
|
||||||
'retrieve pricing data on {dt}. Adjust your dates accordingly.').strip()
|
|
||||||
|
|
||||||
|
|
||||||
class PricingDataNotLoadedError(ZiplineError):
|
|
||||||
msg = ('Missing data for {exchange} {symbols} in date range '
|
|
||||||
'[{start_dt} - {end_dt}]'
|
|
||||||
'\nPlease run: `catalyst ingest-exchange -x {exchange} -f '
|
|
||||||
'{data_frequency} -i {symbol_list}`. 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):
|
|
||||||
msg = ('Unable to fetch candles from the remote API: {error}.').strip()
|
|
||||||
|
|
||||||
|
|
||||||
class NoDataAvailableOnExchange(ZiplineError):
|
|
||||||
msg = (
|
|
||||||
'Requested data for trading pair {symbol} is not available on 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()
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder, MarketOrder
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeLimitOrder(LimitOrder):
|
|
||||||
def get_limit_price(self, is_buy):
|
|
||||||
"""
|
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
is_buy: bool
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
float
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.limit_price
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeStopOrder(StopOrder):
|
|
||||||
def get_stop_price(self, is_buy):
|
|
||||||
"""
|
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
is_buy: bool
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
float
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.stop_price
|
|
||||||
|
|
||||||
|
|
||||||
class ExchangeStopLimitOrder(StopLimitOrder):
|
|
||||||
def get_limit_price(self, is_buy):
|
|
||||||
"""
|
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
is_buy: bool
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
float
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.limit_price
|
|
||||||
|
|
||||||
def get_stop_price(self, is_buy):
|
|
||||||
"""
|
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
is_buy: bool
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
float
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.stop_price
|
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.protocol import Portfolio, Positions, Position
|
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. This fills the role
|
include additional stats in the portfolio object.
|
||||||
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
|
||||||
@@ -30,15 +28,10 @@ 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
|
self.open_orders[order.id] = order
|
||||||
|
|
||||||
@@ -53,18 +46,6 @@ class ExchangePortfolio(Portfolio):
|
|||||||
log.debug('open order added to portfolio')
|
log.debug('open order added to portfolio')
|
||||||
|
|
||||||
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]
|
del self.open_orders[order.id]
|
||||||
|
|
||||||
@@ -90,14 +71,6 @@ class ExchangePortfolio(Portfolio):
|
|||||||
log.debug('updated portfolio with executed order')
|
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]
|
del self.open_orders[order.id]
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +1,18 @@
|
|||||||
import hashlib
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
import urllib
|
||||||
import shutil
|
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
import pandas as pd
|
from catalyst.exchange.exchange_errors import ExchangeAuthNotFound, \
|
||||||
from catalyst.assets._assets import TradingPair
|
ExchangeSymbolsNotFound
|
||||||
from six import string_types
|
from catalyst.utils.paths import data_root, ensure_directory
|
||||||
from six.moves.urllib import request
|
|
||||||
|
|
||||||
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
|
SYMBOLS_URL = 'https://raw.githubusercontent.com/enigmampc/catalyst/' \
|
||||||
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \
|
'live-trading/catalyst/exchange/symbols/{exchange}.json'
|
||||||
InvalidHistoryFrequencyError, InvalidHistoryFrequencyAlias
|
|
||||||
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):
|
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:
|
if not environ:
|
||||||
environ = os.environ
|
environ = os.environ
|
||||||
|
|
||||||
@@ -62,89 +23,26 @@ def get_exchange_folder(exchange_name, environ=None):
|
|||||||
return exchange_folder
|
return exchange_folder
|
||||||
|
|
||||||
|
|
||||||
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):
|
def download_exchange_symbols(exchange_name, environ=None):
|
||||||
"""
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
Downloads the exchange's symbols.json from the repository.
|
filename = os.path.join(exchange_folder, 'symbols.json')
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
exchange_name: str
|
|
||||||
environ:
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
|
|
||||||
"""
|
|
||||||
filename = get_exchange_symbols_filename(exchange_name)
|
|
||||||
url = SYMBOLS_URL.format(exchange=exchange_name)
|
url = SYMBOLS_URL.format(exchange=exchange_name)
|
||||||
response = request.urlretrieve(url=url, filename=filename)
|
response = urllib.urlretrieve(url=url, filename=filename)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def symbols_parser(asset_def):
|
def get_exchange_symbols(exchange_name, environ=None):
|
||||||
for key, value in asset_def.items():
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
match = isinstance(value, string_types) \
|
filename = os.path.join(exchange_folder, 'symbols.json')
|
||||||
and re.search(r'(\d{4}-\d{2}-\d{2})', value)
|
|
||||||
|
|
||||||
if match:
|
if not os.path.isfile(filename):
|
||||||
try:
|
|
||||||
asset_def[key] = pd.to_datetime(value, utc=True)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return asset_def
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
download_exchange_symbols(exchange_name, environ)
|
download_exchange_symbols(exchange_name, environ)
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename) as data_file:
|
with open(filename) as data_file:
|
||||||
try:
|
data = json.load(data_file)
|
||||||
data = json.load(data_file, object_hook=symbols_parser)
|
return data
|
||||||
return data
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
return dict()
|
|
||||||
else:
|
else:
|
||||||
raise ExchangeSymbolsNotFound(
|
raise ExchangeSymbolsNotFound(
|
||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
@@ -152,63 +50,7 @@ def get_exchange_symbols(exchange_name, is_local=False, environ=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
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, environ=None):
|
def get_exchange_auth(exchange_name, 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)
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
filename = os.path.join(exchange_folder, 'auth.json')
|
filename = os.path.join(exchange_folder, 'auth.json')
|
||||||
|
|
||||||
@@ -217,45 +59,13 @@ def get_exchange_auth(exchange_name, environ=None):
|
|||||||
data = json.load(data_file)
|
data = json.load(data_file)
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
data = dict(name=exchange_name, key='', secret='')
|
raise ExchangeAuthNotFound(
|
||||||
with open(filename, 'w') as f:
|
exchange=exchange_name,
|
||||||
json.dump(data, f, sort_keys=False, indent=2,
|
filename=filename
|
||||||
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):
|
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:
|
if not environ:
|
||||||
environ = os.environ
|
environ = os.environ
|
||||||
|
|
||||||
@@ -267,24 +77,6 @@ def get_algo_folder(algo_name, environ=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_algo_object(algo_name, key, environ=None, rel_path=None):
|
def get_algo_object(algo_name, key, environ=None, rel_path=None):
|
||||||
"""
|
|
||||||
The de-serialized object of the algo name and key.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
algo_name: str
|
|
||||||
key: str
|
|
||||||
environ:
|
|
||||||
rel_path: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Object
|
|
||||||
|
|
||||||
"""
|
|
||||||
if algo_name is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
folder = get_algo_folder(algo_name, environ)
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
|
||||||
if rel_path is not None:
|
if rel_path is not None:
|
||||||
@@ -303,18 +95,6 @@ def get_algo_object(algo_name, key, environ=None, rel_path=None):
|
|||||||
|
|
||||||
|
|
||||||
def save_algo_object(algo_name, key, obj, environ=None, rel_path=None):
|
def save_algo_object(algo_name, key, obj, environ=None, rel_path=None):
|
||||||
"""
|
|
||||||
Serialize and save an object by algo name and key.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
algo_name: str
|
|
||||||
key: str
|
|
||||||
obj: Object
|
|
||||||
environ:
|
|
||||||
rel_path: str
|
|
||||||
|
|
||||||
"""
|
|
||||||
folder = get_algo_folder(algo_name, environ)
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
|
||||||
if rel_path is not None:
|
if rel_path is not None:
|
||||||
@@ -327,77 +107,16 @@ def save_algo_object(algo_name, key, obj, environ=None, rel_path=None):
|
|||||||
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
|
|
||||||
|
|
||||||
def get_algo_df(algo_name, key, environ=None, rel_path=None):
|
def append_algo_object(algo_name, key, obj, environ=None):
|
||||||
"""
|
algo_folder = get_algo_folder(algo_name, environ)
|
||||||
The de-serialized DataFrame of an algo name and key.
|
filename = os.path.join(algo_folder, key + '.p')
|
||||||
|
|
||||||
Parameters
|
mode = 'a+b' if os.path.isfile(filename) else 'wb'
|
||||||
----------
|
with open(filename, mode) as handle:
|
||||||
algo_name: str
|
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
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 get_exchange_minute_writer_root(exchange_name, environ=None):
|
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)
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
|
|
||||||
minute_data_folder = os.path.join(exchange_folder, 'minute_data')
|
minute_data_folder = os.path.join(exchange_folder, 'minute_data')
|
||||||
@@ -406,224 +125,9 @@ def get_exchange_minute_writer_root(exchange_name, environ=None):
|
|||||||
return 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 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):
|
def perf_serial(obj):
|
||||||
"""
|
"""JSON serializer for objects not serializable by default json code"""
|
||||||
JSON serializer for objects not serializable by default json code
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
obj: Object
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
|
|
||||||
"""
|
|
||||||
if isinstance(obj, (datetime, date)):
|
if isinstance(obj, (datetime, date)):
|
||||||
return obj.isoformat()
|
return obj.isoformat()
|
||||||
|
|
||||||
raise TypeError("Type %s not serializable" % type(obj))
|
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 get_frequency(freq, data_frequency):
|
|
||||||
"""
|
|
||||||
Get the frequency parameters.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
We're trying to use Pandas convention for frequency aliases.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
freq: str
|
|
||||||
data_frequency: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str, int, str, str
|
|
||||||
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
|
|
||||||
if unit.lower() == 'd':
|
|
||||||
alias = '{}D'.format(candle_size)
|
|
||||||
|
|
||||||
if data_frequency == 'minute':
|
|
||||||
data_frequency = 'daily'
|
|
||||||
|
|
||||||
elif unit.lower() == 'm' or unit == 'T':
|
|
||||||
alias = '{}T'.format(candle_size)
|
|
||||||
|
|
||||||
if data_frequency == 'daily':
|
|
||||||
data_frequency = 'minute'
|
|
||||||
|
|
||||||
# elif unit.lower() == 'h':
|
|
||||||
# candle_size = candle_size * 60
|
|
||||||
#
|
|
||||||
# alias = '{}T'.format(candle_size)
|
|
||||||
# if data_frequency == 'daily':
|
|
||||||
# data_frequency = 'minute'
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise InvalidHistoryFrequencyAlias(freq=freq)
|
|
||||||
|
|
||||||
return alias, candle_size, unit, data_frequency
|
|
||||||
|
|
||||||
|
|
||||||
def resample_history_df(df, freq, field):
|
|
||||||
"""
|
|
||||||
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).agg(agg)
|
|
||||||
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']
|
|
||||||
|
|
||||||
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'])
|
|
||||||
|
|
||||||
|
|
||||||
def from_ms_timestamp(ms):
|
|
||||||
return pd.to_datetime(ms, unit='ms', utc=True)
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
|
||||||
from catalyst.exchange.exchange_errors import ExchangeAuthEmpty
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_auth, \
|
|
||||||
get_exchange_folder
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchange(exchange_name, base_currency=None, portfolio=None,
|
|
||||||
must_authenticate=False):
|
|
||||||
exchange_auth = get_exchange_auth(exchange_name)
|
|
||||||
|
|
||||||
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'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return CCXT(
|
|
||||||
exchange_name=exchange_name,
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'],
|
|
||||||
base_currency=base_currency,
|
|
||||||
portfolio=portfolio
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_exchanges(exchange_names):
|
|
||||||
exchanges = dict()
|
|
||||||
for exchange_name in exchange_names:
|
|
||||||
exchanges[exchange_name] = get_exchange(exchange_name)
|
|
||||||
|
|
||||||
return exchanges
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
from catalyst.gens.sim_engine import (
|
|
||||||
BAR,
|
|
||||||
SESSION_START
|
|
||||||
)
|
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.exchange.exchange_errors import \
|
|
||||||
MismatchingBaseCurrenciesExchanges
|
|
||||||
|
|
||||||
log = Logger('LiveGraphClock', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class LiveGraphClock(object):
|
|
||||||
"""Realtime clock for live trading.
|
|
||||||
|
|
||||||
This class is a drop-in replacement for
|
|
||||||
:class:`zipline.gens.sim_engine.MinuteSimulationClock`.
|
|
||||||
|
|
||||||
This mixes the clock with a live graph.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
This seemingly awkward approach allows us to run the program using a single
|
|
||||||
thread. This is important because Matplotlib does not play nice with
|
|
||||||
multi-threaded environments. Zipline probably does not either.
|
|
||||||
|
|
||||||
|
|
||||||
Matplotlib has a pause() method which is a wrapper around time.sleep()
|
|
||||||
used in the SimpleClock. The key difference is that users
|
|
||||||
can still interact with the chart during the pause cycles. This is
|
|
||||||
what enables us to keep a single thread. This is also why we are not using
|
|
||||||
the 'animate' callback of Matplotlib. We need to direct access to the
|
|
||||||
__iter__ method in order to yield events to Zipline.
|
|
||||||
|
|
||||||
The :param:`time_skew` parameter represents the time difference between
|
|
||||||
the exchange and the live trading machine's clock. It's not used currently.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, sessions, context, 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.time_skew = time_skew
|
|
||||||
self._last_emit = None
|
|
||||||
self._before_trading_start_bar_yielded = True
|
|
||||||
self.context = context
|
|
||||||
self.fmt = mdates.DateFormatter('%Y-%m-%d %H:%M')
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
ax:
|
|
||||||
|
|
||||||
"""
|
|
||||||
# TODO: room for improvement
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
Set legend on the chart.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
ax
|
|
||||||
|
|
||||||
"""
|
|
||||||
ax.legend(loc='upper left', ncol=1, fontsize=10, numpoints=1)
|
|
||||||
|
|
||||||
def draw_pnl(self):
|
|
||||||
"""
|
|
||||||
Draw p&l line on the chart.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
Draw custom signals on the chart.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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):
|
|
||||||
"""
|
|
||||||
Draw exposure line on the chart.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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):
|
|
||||||
yield pd.Timestamp.utcnow(), SESSION_START
|
|
||||||
|
|
||||||
while True:
|
|
||||||
current_time = pd.Timestamp.utcnow()
|
|
||||||
current_minute = current_time.floor('1 min')
|
|
||||||
|
|
||||||
if self._last_emit is None or current_minute > self._last_emit:
|
|
||||||
log.debug('emitting minutely bar: {}'.format(current_minute))
|
|
||||||
|
|
||||||
self._last_emit = current_minute
|
|
||||||
yield current_minute, BAR
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.draw_pnl()
|
|
||||||
self.draw_custom_signals()
|
|
||||||
self.draw_exposure()
|
|
||||||
|
|
||||||
plt.draw()
|
|
||||||
except Exception as e:
|
|
||||||
log.warn('Unable to update the graph: {}'.format(e))
|
|
||||||
|
|
||||||
else:
|
|
||||||
# I can't use the "animate" reactive approach here because
|
|
||||||
# I need to yield from the main loop.
|
|
||||||
|
|
||||||
# Workaround: https://stackoverflow.com/a/33050617/814633
|
|
||||||
plt.pause(1)
|
|
||||||
@@ -1,655 +0,0 @@
|
|||||||
import json
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
import pytz
|
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
|
||||||
# import six
|
|
||||||
from six import iteritems
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
# from websocket import create_connection
|
|
||||||
from catalyst.exchange.exchange import Exchange
|
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
|
||||||
from catalyst.exchange.exchange_errors import (
|
|
||||||
ExchangeRequestError,
|
|
||||||
InvalidHistoryFrequencyError,
|
|
||||||
InvalidOrderStyle, OrphanOrderReverseError)
|
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
|
|
||||||
ExchangeStopLimitOrder
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
|
|
||||||
download_exchange_symbols, get_symbols_string
|
|
||||||
from catalyst.exchange.poloniex.poloniex_api import Poloniex_api
|
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
|
||||||
from catalyst.finance.transaction import Transaction
|
|
||||||
from catalyst.protocol import Account
|
|
||||||
|
|
||||||
log = Logger('Poloniex', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class Poloniex(Exchange):
|
|
||||||
def __init__(self, key, secret, base_currency, portfolio=None):
|
|
||||||
self.api = Poloniex_api(key=key, secret=secret)
|
|
||||||
self.name = 'poloniex'
|
|
||||||
|
|
||||||
self.assets = dict()
|
|
||||||
self.load_assets()
|
|
||||||
|
|
||||||
self.local_assets = dict()
|
|
||||||
self.load_assets(is_local=True)
|
|
||||||
|
|
||||||
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.name)
|
|
||||||
|
|
||||||
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):
|
|
||||||
balances = self.api.returnbalances()
|
|
||||||
try:
|
|
||||||
log.debug('retrieving wallets balances')
|
|
||||||
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, freq, assets, bar_count=None,
|
|
||||||
start_dt=None, end_dt=None):
|
|
||||||
"""
|
|
||||||
Retrieve OHLVC candles from Poloniex
|
|
||||||
|
|
||||||
:param freq:
|
|
||||||
:param assets:
|
|
||||||
:param bar_count:
|
|
||||||
:return:
|
|
||||||
|
|
||||||
Available Frequencies
|
|
||||||
---------------------
|
|
||||||
'5m', '15m', '30m', '2h', '4h', '1D'
|
|
||||||
"""
|
|
||||||
|
|
||||||
if end_dt is None:
|
|
||||||
end_dt = pd.Timestamp.utcnow()
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
'retrieving {bars} {freq} candles on {exchange} from '
|
|
||||||
'{end_dt} for markets {symbols}, '.format(
|
|
||||||
bars=bar_count,
|
|
||||||
freq=freq,
|
|
||||||
exchange=self.name,
|
|
||||||
end_dt=end_dt,
|
|
||||||
symbols=get_symbols_string(assets)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if freq == '1T' and (bar_count == 1 or bar_count is None):
|
|
||||||
# TODO: use the order book instead
|
|
||||||
# We use the 5m to fetch the last bar
|
|
||||||
frequency = 300
|
|
||||||
elif freq == '5T':
|
|
||||||
frequency = 300
|
|
||||||
elif freq == '15T':
|
|
||||||
frequency = 900
|
|
||||||
elif freq == '30T':
|
|
||||||
frequency = 1800
|
|
||||||
elif freq == '120T':
|
|
||||||
frequency = 7200
|
|
||||||
elif freq == '240T':
|
|
||||||
frequency = 14400
|
|
||||||
elif freq == '1D':
|
|
||||||
frequency = 86400
|
|
||||||
else:
|
|
||||||
# Poloniex does not offer 1m data candles
|
|
||||||
# It is likely to error out there frequently
|
|
||||||
raise InvalidHistoryFrequencyError(frequency=freq)
|
|
||||||
|
|
||||||
# Making sure that assets are iterable
|
|
||||||
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
|
||||||
ohlc_map = dict()
|
|
||||||
|
|
||||||
for asset in asset_list:
|
|
||||||
delta = end_dt - pd.to_datetime('1970-1-1', utc=True)
|
|
||||||
end = int(delta.total_seconds())
|
|
||||||
|
|
||||||
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,215 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
import ssl
|
|
||||||
|
|
||||||
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 = list(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:
|
|
||||||
|
|
||||||
time.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.encode('utf-8'),
|
|
||||||
post_data.encode('utf-8'),
|
|
||||||
hashlib.sha512).hexdigest()
|
|
||||||
headers = {'Sign': signature, 'Key': self.key}
|
|
||||||
|
|
||||||
post_data = post_data.encode('utf-8')
|
|
||||||
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, context=ssl._create_unverified_context()).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')
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import numbers
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
|
|
||||||
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 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.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
stats_df: DataFrame
|
|
||||||
num_rows: int
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
|
|
||||||
"""
|
|
||||||
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}{base} cost basis {cost_basis:.4f}{quote}'.format(
|
|
||||||
amount=position['amount'],
|
|
||||||
base=position['sid'].base_currency,
|
|
||||||
cost_basis=position['cost_basis'],
|
|
||||||
quote=position['sid'].quote_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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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_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
|
|
||||||
@@ -1,25 +1,8 @@
|
|||||||
{
|
{
|
||||||
"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": {
|
"btcusd": {
|
||||||
"symbol": "btc_usd",
|
"symbol": "btc_usd",
|
||||||
"start_date": "2010-01-01"
|
"start_date": "2010-01-01"
|
||||||
},
|
},
|
||||||
"bchusd": {
|
|
||||||
"symbol": "bch_usd",
|
|
||||||
"start_date": "2010-01-01"
|
|
||||||
},
|
|
||||||
"ltcusd": {
|
"ltcusd": {
|
||||||
"symbol": "ltc_usd",
|
"symbol": "ltc_usd",
|
||||||
"start_date": "2010-01-01"
|
"start_date": "2010-01-01"
|
||||||
@@ -30,19 +13,19 @@
|
|||||||
},
|
},
|
||||||
"ethusd": {
|
"ethusd": {
|
||||||
"symbol": "eth_usd",
|
"symbol": "eth_usd",
|
||||||
"start_date": "2017-01-01"
|
"start_date": "2010-01-01"
|
||||||
},
|
},
|
||||||
"ethbtc": {
|
"ethbtc": {
|
||||||
"symbol": "eth_btc",
|
"symbol": "eth_btc",
|
||||||
"start_date": "2017-01-01"
|
"start_date": "2010-01-01"
|
||||||
},
|
},
|
||||||
"etcbtc": {
|
"etcbtc": {
|
||||||
"symbol": "etc_btc",
|
"symbol": "etc_btc",
|
||||||
"start_date": "2017-01-01"
|
"start_date": "2010-01-01"
|
||||||
},
|
},
|
||||||
"etcusd": {
|
"etcusd": {
|
||||||
"symbol": "etc_usd",
|
"symbol": "etc_usd",
|
||||||
"start_date": "2017-01-01"
|
"start_date": "2010-01-01"
|
||||||
},
|
},
|
||||||
"rrtusd": {
|
"rrtusd": {
|
||||||
"symbol": "rrt_usd",
|
"symbol": "rrt_usd",
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import pandas as pd
|
|
||||||
import six
|
|
||||||
from catalyst.assets._assets import TradingPair, get_calendar
|
|
||||||
from logbook import Logger
|
|
||||||
from pandas.util.testing import assert_frame_equal
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
|
|
||||||
from catalyst.exchange.exchange_data_portal import DataPortalExchangeBacktest
|
|
||||||
from catalyst.exchange.factory import get_exchanges
|
|
||||||
from catalyst.utils.paths import ensure_directory
|
|
||||||
|
|
||||||
log = Logger('Validator', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
exchange_folder = assets.exchange
|
|
||||||
asset_folder = assets.symbol
|
|
||||||
else:
|
|
||||||
exchange_folder = ','.join([asset.exchange for asset in assets])
|
|
||||||
asset_folder = ','.join([asset.symbol for asset in assets])
|
|
||||||
|
|
||||||
folder = os.path.join(
|
|
||||||
tempfile.gettempdir(), 'catalyst', exchange_folder, 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
|
|
||||||
|
|
||||||
|
|
||||||
class Validator(object):
|
|
||||||
def __init__(self, data_portal):
|
|
||||||
self.data_portal = data_portal
|
|
||||||
|
|
||||||
def compare_bundle_with_exchange(self, exchange, assets, end_dt, bar_count,
|
|
||||||
sample_minutes):
|
|
||||||
"""
|
|
||||||
Creates DataFrames from the bundle and exchange for the specified
|
|
||||||
data set.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
exchange: Exchange
|
|
||||||
assets
|
|
||||||
end_dt
|
|
||||||
bar_count
|
|
||||||
sample_minutes
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
|
|
||||||
"""
|
|
||||||
freq = '{}T'.format(sample_minutes)
|
|
||||||
|
|
||||||
log.info('creating data sample from bundle')
|
|
||||||
df1 = self.data_portal.get_history_window(
|
|
||||||
assets=assets,
|
|
||||||
end_dt=end_dt,
|
|
||||||
bar_count=bar_count,
|
|
||||||
frequency=freq,
|
|
||||||
field='close',
|
|
||||||
data_frequency='minute'
|
|
||||||
)
|
|
||||||
path = output_df(df1, assets, '{}_resampled'.format(freq))
|
|
||||||
log.info('saved resampled bundle candles: {}\n{}'.format(
|
|
||||||
path, df1.tail(10))
|
|
||||||
)
|
|
||||||
|
|
||||||
log.info('creating data sample from exchange api')
|
|
||||||
candles = exchange.get_candles(
|
|
||||||
end_dt=end_dt,
|
|
||||||
freq='{}T'.format(sample_minutes),
|
|
||||||
assets=assets,
|
|
||||||
bar_count=bar_count
|
|
||||||
)
|
|
||||||
|
|
||||||
series = dict()
|
|
||||||
for asset in assets:
|
|
||||||
series[asset] = pd.Series(
|
|
||||||
data=[candle['close'] for candle in candles[asset]],
|
|
||||||
index=[candle['last_traded'] for candle in candles[asset]]
|
|
||||||
)
|
|
||||||
|
|
||||||
df2 = pd.DataFrame(series)
|
|
||||||
path = output_df(df2, assets, '{}_api'.format(freq))
|
|
||||||
log.info('saved exchange api candles: {}\n{}'.format(
|
|
||||||
path, df2.tail(10))
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
assert_frame_equal(df1, df2)
|
|
||||||
return True
|
|
||||||
except:
|
|
||||||
log.warn('differences found in dataframes')
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
exchanges = get_exchanges(['poloniex'])
|
|
||||||
exchange = six.next(six.itervalues(exchanges))
|
|
||||||
assets = exchange.get_assets(symbols=['eth_btc'])
|
|
||||||
|
|
||||||
open_calendar = get_calendar('OPEN')
|
|
||||||
asset_finder = AssetFinderExchange()
|
|
||||||
data_portal = DataPortalExchangeBacktest(
|
|
||||||
exchanges=exchanges,
|
|
||||||
asset_finder=asset_finder,
|
|
||||||
trading_calendar=open_calendar,
|
|
||||||
first_trading_day=None # will set dynamically based on assets
|
|
||||||
)
|
|
||||||
validator = Validator(data_portal=data_portal)
|
|
||||||
|
|
||||||
validator.compare_bundle_with_exchange(
|
|
||||||
exchange=exchange,
|
|
||||||
assets=assets,
|
|
||||||
end_dt=pd.to_datetime('2017-11-10 1:00', utc=True),
|
|
||||||
bar_count=200,
|
|
||||||
sample_minutes=30
|
|
||||||
)
|
|
||||||
@@ -34,9 +34,7 @@ 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
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = Logger('Blotter')
|
||||||
|
|
||||||
log = Logger('Blotter', level=LOG_LEVEL)
|
|
||||||
warning_logger = Logger('AlgoWarning')
|
warning_logger = Logger('AlgoWarning')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ from catalyst.errors import (
|
|||||||
TradingControlViolation,
|
TradingControlViolation,
|
||||||
)
|
)
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = logbook.Logger('TradingControl')
|
||||||
|
|
||||||
log = logbook.Logger('TradingControl', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class TradingControl(with_metaclass(abc.ABCMeta)):
|
class TradingControl(with_metaclass(abc.ABCMeta)):
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ 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.
|
||||||
@@ -100,7 +99,6 @@ 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.
|
||||||
@@ -123,7 +121,6 @@ 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
|
||||||
@@ -147,20 +144,31 @@ 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)):
|
||||||
"""
|
"""
|
||||||
Modified the original function because we do not want to round
|
Asymmetric rounding function for adjusting prices to two places in a way
|
||||||
prices on crypto exchange.
|
that "improves" the price. For limit prices, this means preferring to
|
||||||
|
round down on buys and preferring to round up on sells. For stop prices,
|
||||||
|
it means the reverse.
|
||||||
|
|
||||||
Parameters
|
If prefer_round_down == True:
|
||||||
----------
|
When .05 below to .95 above a penny, use that penny.
|
||||||
price: float
|
If prefer_round_down == False:
|
||||||
|
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.
|
||||||
"""
|
"""
|
||||||
# TODO: consider overriding outside of the original function
|
# Subtracting an epsilon from diff to enforce the open-ness of the upper
|
||||||
return price
|
# bound on buys and the lower bound on sells. Using the actual system
|
||||||
|
# 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,10 +88,7 @@ from six import itervalues, iteritems
|
|||||||
|
|
||||||
import catalyst.protocol as zp
|
import catalyst.protocol as zp
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = logbook.Logger('Performance')
|
||||||
|
|
||||||
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE
|
TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ 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
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = logbook.Logger('Performance')
|
||||||
|
|
||||||
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class Position(object):
|
class Position(object):
|
||||||
|
|||||||
@@ -32,9 +32,7 @@ from catalyst.assets import (
|
|||||||
)
|
)
|
||||||
from . position import positiondict
|
from . position import positiondict
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = logbook.Logger('Performance')
|
||||||
|
|
||||||
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
PositionStats = namedtuple('PositionStats',
|
PositionStats = namedtuple('PositionStats',
|
||||||
|
|||||||
@@ -70,9 +70,7 @@ import catalyst.finance.risk as risk
|
|||||||
|
|
||||||
from . position_tracker import PositionTracker
|
from . position_tracker import PositionTracker
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = logbook.Logger('Performance')
|
||||||
|
|
||||||
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class PerformanceTracker(object):
|
class PerformanceTracker(object):
|
||||||
@@ -113,11 +111,27 @@ class PerformanceTracker(object):
|
|||||||
self.treasury_curves,
|
self.treasury_curves,
|
||||||
self.trading_calendar
|
self.trading_calendar
|
||||||
)
|
)
|
||||||
|
elif self.emission_rate == '5-minute':
|
||||||
|
self.all_benchmark_returns = pd.Series(
|
||||||
|
index=pd.date_range(
|
||||||
|
self.sim_params.first_open,
|
||||||
|
self.sim_params.last_close,
|
||||||
|
freq='5min'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.cumulative_risk_metrics = \
|
||||||
|
risk.RiskMetricsCumulative(
|
||||||
|
self.sim_params,
|
||||||
|
self.treasury_curves,
|
||||||
|
self.trading_calendar,
|
||||||
|
create_first_day_stats=True,
|
||||||
|
)
|
||||||
elif self.emission_rate == 'minute':
|
elif self.emission_rate == 'minute':
|
||||||
self.all_benchmark_returns = pd.Series(index=pd.date_range(
|
self.all_benchmark_returns = pd.Series(index=pd.date_range(
|
||||||
self.sim_params.first_open, self.sim_params.last_close,
|
self.sim_params.first_open, self.sim_params.last_close,
|
||||||
freq='Min')
|
freq='Min')
|
||||||
)
|
)
|
||||||
|
|
||||||
self.cumulative_risk_metrics = \
|
self.cumulative_risk_metrics = \
|
||||||
risk.RiskMetricsCumulative(
|
risk.RiskMetricsCumulative(
|
||||||
self.sim_params,
|
self.sim_params,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ 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
|
||||||
)
|
)
|
||||||
@@ -37,10 +37,9 @@ from empyrical import (
|
|||||||
sharpe_ratio,
|
sharpe_ratio,
|
||||||
sortino_ratio,
|
sortino_ratio,
|
||||||
)
|
)
|
||||||
import warnings
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
|
||||||
|
|
||||||
log = logbook.Logger('Risk Cumulative', level=LOG_LEVEL)
|
log = logbook.Logger('Risk Cumulative')
|
||||||
|
|
||||||
|
|
||||||
choose_treasury = functools.partial(choose_treasury, lambda *args: '10year',
|
choose_treasury = functools.partial(choose_treasury, lambda *args: '10year',
|
||||||
compound=False)
|
compound=False)
|
||||||
@@ -144,8 +143,6 @@ 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
|
||||||
@@ -192,12 +189,9 @@ 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)
|
||||||
|
|
||||||
try:
|
self.benchmark_cumulative_returns[dt_loc] = cum_returns(
|
||||||
self.benchmark_cumulative_returns[dt_loc] = cum_returns(
|
self.benchmark_returns
|
||||||
self.benchmark_returns
|
)[-1]
|
||||||
)[-1]
|
|
||||||
except Exception:
|
|
||||||
self.benchmark_cumulative_returns[dt_loc] = 0
|
|
||||||
|
|
||||||
benchmark_cumulative_returns_to_date = \
|
benchmark_cumulative_returns_to_date = \
|
||||||
self.benchmark_cumulative_returns[:dt_loc + 1]
|
self.benchmark_cumulative_returns[:dt_loc + 1]
|
||||||
@@ -272,17 +266,10 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
|
|||||||
self.downside_risk[dt_loc] = downside_risk(
|
self.downside_risk[dt_loc] = downside_risk(
|
||||||
self.algorithm_returns
|
self.algorithm_returns
|
||||||
)
|
)
|
||||||
|
self.sortino[dt_loc] = sortino_ratio(
|
||||||
try:
|
self.algorithm_returns,
|
||||||
risk = self.downside_risk[dt_loc]
|
_downside_risk=self.downside_risk[dt_loc]
|
||||||
self.sortino[dt_loc] = sortino_ratio(
|
)
|
||||||
self.algorithm_returns,
|
|
||||||
_downside_risk=risk
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
# TODO: what causes it to error out?
|
|
||||||
self.sortino[dt_loc] = 0
|
|
||||||
|
|
||||||
self.information[dt_loc] = information_ratio(
|
self.information[dt_loc] = information_ratio(
|
||||||
self.algorithm_returns,
|
self.algorithm_returns,
|
||||||
self.benchmark_returns,
|
self.benchmark_returns,
|
||||||
@@ -294,8 +281,6 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}"
|
|||||||
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.
|
||||||
@@ -307,18 +292,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,7 +14,6 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import warnings
|
|
||||||
|
|
||||||
import logbook
|
import logbook
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ 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,
|
||||||
@@ -37,9 +36,7 @@ from empyrical import (
|
|||||||
sortino_ratio
|
sortino_ratio
|
||||||
)
|
)
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = logbook.Logger('Risk Period')
|
||||||
|
|
||||||
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)
|
||||||
@@ -79,20 +76,14 @@ class RiskMetricsPeriod(object):
|
|||||||
self.calculate_metrics()
|
self.calculate_metrics()
|
||||||
|
|
||||||
def calculate_metrics(self):
|
def calculate_metrics(self):
|
||||||
warnings.filterwarnings('error')
|
self.benchmark_period_returns = \
|
||||||
|
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}"
|
||||||
@@ -135,17 +126,10 @@ 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(
|
||||||
try:
|
self.algorithm_returns.values,
|
||||||
risk = self.downside_risk
|
_downside_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,
|
||||||
@@ -155,12 +139,10 @@ class RiskMetricsPeriod(object):
|
|||||||
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,9 +63,7 @@ from dateutil.relativedelta import relativedelta
|
|||||||
|
|
||||||
from . period import RiskMetricsPeriod
|
from . period import RiskMetricsPeriod
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = logbook.Logger('Risk Report')
|
||||||
|
|
||||||
log = logbook.Logger('Risk Report', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class RiskReport(object):
|
class RiskReport(object):
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ Risk Report
|
|||||||
import logbook
|
import logbook
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = logbook.Logger('Risk')
|
||||||
|
|
||||||
log = logbook.Logger('Risk', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
TREASURY_DURATIONS = [
|
TREASURY_DURATIONS = [
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -206,22 +205,20 @@ class VolumeShareSlippage(SlippageModel):
|
|||||||
def process_order(self, data, order):
|
def process_order(self, data, order):
|
||||||
volume = data.current(order.asset, "volume")
|
volume = data.current(order.asset, "volume")
|
||||||
|
|
||||||
min_trade_size = order.asset.min_trade_size
|
|
||||||
|
|
||||||
max_volume = self.volume_limit * volume
|
max_volume = self.volume_limit * volume
|
||||||
|
|
||||||
# price impact accounts for the total volume of transactions
|
# price impact accounts for the total volume of transactions
|
||||||
# created against the current minute bar
|
# created against the current minute bar
|
||||||
remaining_volume = max_volume - self.volume_for_bar
|
remaining_volume = max_volume - self.volume_for_bar
|
||||||
if remaining_volume < min_trade_size:
|
if remaining_volume < 1:
|
||||||
# we can't fill any more transactions
|
# we can't fill any more transactions
|
||||||
raise LiquidityExceeded()
|
raise LiquidityExceeded()
|
||||||
|
|
||||||
# the current order amount will be the min of the
|
# the current order amount will be the min of the
|
||||||
# volume available in the bar or the open amount.
|
# volume available in the bar or the open amount.
|
||||||
cur_volume = min(remaining_volume, abs(order.open_amount))
|
cur_volume = int(min(remaining_volume, abs(order.open_amount)))
|
||||||
|
|
||||||
if cur_volume < min_trade_size:
|
if cur_volume < 1:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# tally the current amount into our total amount ordered.
|
# tally the current amount into our total amount ordered.
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ 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
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = logbook.Logger('Trading')
|
||||||
|
|
||||||
log = logbook.Logger('Trading', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CAPITAL_BASE = 1e5
|
DEFAULT_CAPITAL_BASE = 1e5
|
||||||
|
|||||||
@@ -65,10 +65,14 @@ def create_transaction(order, dt, price, amount):
|
|||||||
# floor the amount to protect against non-whole number orders
|
# floor the amount to protect against non-whole number orders
|
||||||
# TODO: Investigate whether we can add a robust check in blotter
|
# TODO: Investigate whether we can add a robust check in blotter
|
||||||
# and/or tradesimulation, as well.
|
# and/or tradesimulation, as well.
|
||||||
|
amount_magnitude = int(abs(amount))
|
||||||
|
|
||||||
|
if amount_magnitude < 1:
|
||||||
|
raise Exception("Transaction magnitude must be at least 1.")
|
||||||
|
|
||||||
transaction = Transaction(
|
transaction = Transaction(
|
||||||
asset=order.asset,
|
asset=order.asset,
|
||||||
amount=amount,
|
amount=int(amount),
|
||||||
dt=dt,
|
dt=dt,
|
||||||
price=price,
|
price=price,
|
||||||
order_id=order.id
|
order_id=order.id
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ cimport cython
|
|||||||
from cpython cimport bool
|
from cpython cimport bool
|
||||||
|
|
||||||
cdef np.int64_t _nanos_in_minute = 60000000000
|
cdef np.int64_t _nanos_in_minute = 60000000000
|
||||||
|
cdef np.int64_t _nanos_in_five_minutes = 5 * _nanos_in_minute
|
||||||
NANOS_IN_MINUTE = _nanos_in_minute
|
NANOS_IN_MINUTE = _nanos_in_minute
|
||||||
|
NANOS_IN_FIVE_MINUTES = _nanos_in_five_minutes
|
||||||
|
|
||||||
cpdef enum:
|
cpdef enum:
|
||||||
BAR = 0
|
BAR = 0
|
||||||
@@ -115,3 +117,24 @@ cdef class MinuteSimulationClock:
|
|||||||
yield minute, BAR
|
yield minute, BAR
|
||||||
if minute_emission:
|
if minute_emission:
|
||||||
yield minute, MINUTE_END
|
yield minute, MINUTE_END
|
||||||
|
|
||||||
|
cdef class FiveMinuteSimulationClock(MinuteSimulationClock):
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cdef dict calc_minutes_by_session(self):
|
||||||
|
cdef dict five_minutes_by_session
|
||||||
|
cdef int session_idx
|
||||||
|
cdef np.int64_t session_nano
|
||||||
|
cdef np.ndarray[np.int64_t, ndim=1] five_minutes_nanos
|
||||||
|
|
||||||
|
five_minutes_by_session = {}
|
||||||
|
for session_idx, session_nano in enumerate(self.sessions_nanos):
|
||||||
|
five_minutes_nanos = np.arange(
|
||||||
|
self.market_opens_nanos[session_idx],
|
||||||
|
self.market_closes_nanos[session_idx],
|
||||||
|
_nanos_in_five_minutes
|
||||||
|
)
|
||||||
|
five_minutes_by_session[session_nano] = pd.to_datetime(
|
||||||
|
five_minutes_nanos, utc=True, box=True
|
||||||
|
)
|
||||||
|
return five_minutes_by_session
|
||||||
|
|||||||
@@ -27,15 +27,14 @@ from catalyst.gens.sim_engine import (
|
|||||||
BEFORE_TRADING_START_BAR
|
BEFORE_TRADING_START_BAR
|
||||||
)
|
)
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
log = Logger('Trade Simulation')
|
||||||
|
|
||||||
log = Logger('Trade Simulation', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
class AlgorithmSimulator(object):
|
class AlgorithmSimulator(object):
|
||||||
|
|
||||||
EMISSION_TO_PERF_KEY_MAP = {
|
EMISSION_TO_PERF_KEY_MAP = {
|
||||||
'minute': 'minute_perf',
|
'minute': 'minute_perf',
|
||||||
|
'5-minute': '5_minute_perf',
|
||||||
'daily': 'daily_perf'
|
'daily': 'daily_perf'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +202,7 @@ class AlgorithmSimulator(object):
|
|||||||
stack.enter_context(self.processor)
|
stack.enter_context(self.processor)
|
||||||
stack.enter_context(ZiplineAPI(self.algo))
|
stack.enter_context(ZiplineAPI(self.algo))
|
||||||
|
|
||||||
if algo.data_frequency == 'minute':
|
if algo.data_frequency in set(('minute', '5-minute')):
|
||||||
def execute_order_cancellation_policy():
|
def execute_order_cancellation_policy():
|
||||||
algo.blotter.execute_cancel_policy(SESSION_END)
|
algo.blotter.execute_cancel_policy(SESSION_END)
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ class CryptoPricingLoader(PipelineLoader):
|
|||||||
reader = bundle.daily_bar_reader
|
reader = bundle.daily_bar_reader
|
||||||
all_sessions = cal.all_sessions
|
all_sessions = cal.all_sessions
|
||||||
|
|
||||||
|
elif data_frequency == '5-minute':
|
||||||
|
reader = bundle.five_minute_bar_reader
|
||||||
|
all_sessions = cal.all_five_minutes
|
||||||
|
|
||||||
elif data_frequency == 'minute':
|
elif data_frequency == 'minute':
|
||||||
reader = bundle.minute_bar_reader
|
reader = bundle.minute_bar_reader
|
||||||
all_sessions = cal.all_minutes
|
all_sessions = cal.all_minutes
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ class USEquityPricingLoader(PipelineLoader):
|
|||||||
|
|
||||||
if data_frequency == 'daily':
|
if data_frequency == 'daily':
|
||||||
reader = bundle.daily_bar_reader
|
reader = bundle.daily_bar_reader
|
||||||
|
elif data_frequency == '5-minute':
|
||||||
|
reader = bundle.five_minute_bar_reader
|
||||||
elif daily_bar_reader == 'minute':
|
elif daily_bar_reader == 'minute':
|
||||||
reader = bundle.minute_bar_reader
|
reader = bundle.minute_bar_reader
|
||||||
else:
|
else:
|
||||||
@@ -51,6 +53,9 @@ class USEquityPricingLoader(PipelineLoader):
|
|||||||
|
|
||||||
if data_frequency == 'daily':
|
if data_frequency == 'daily':
|
||||||
all_sessions = cal.all_sessions
|
all_sessions = cal.all_sessions
|
||||||
|
elif data_frequency == '5-minute':
|
||||||
|
reader = bundle.five_minute_bar_reader
|
||||||
|
all_sessions = cal.all_five_minutes
|
||||||
elif daily_bar_reader == 'minute':
|
elif daily_bar_reader == 'minute':
|
||||||
reader = bundle.minute_bar_reader
|
reader = bundle.minute_bar_reader
|
||||||
all_sessions = cal.all_minutes
|
all_sessions = cal.all_minutes
|
||||||
|
|||||||
@@ -65,6 +65,19 @@ class BenchmarkSource(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._precalculated_series = minute_series
|
self._precalculated_series = minute_series
|
||||||
|
elif self.emission_rate == '5-minute':
|
||||||
|
five_minutes = \
|
||||||
|
trading_calendar.five_minutes_for_sessions_in_range(
|
||||||
|
sessions[0],
|
||||||
|
sessions[-1],
|
||||||
|
)
|
||||||
|
|
||||||
|
five_minute_series = daily_series.reindex(
|
||||||
|
index=five_minutes,
|
||||||
|
method='ffill',
|
||||||
|
)
|
||||||
|
|
||||||
|
self._precalculated_series = five_minute_series
|
||||||
else:
|
else:
|
||||||
self._precalculated_series = daily_series
|
self._precalculated_series = daily_series
|
||||||
else:
|
else:
|
||||||
@@ -72,13 +85,7 @@ class BenchmarkSource(object):
|
|||||||
"benchmark_returns.")
|
"benchmark_returns.")
|
||||||
|
|
||||||
def get_value(self, dt):
|
def get_value(self, dt):
|
||||||
try:
|
return self._precalculated_series.loc[dt]
|
||||||
series = self._precalculated_series
|
|
||||||
value = series.loc[dt]
|
|
||||||
return value
|
|
||||||
except Exception:
|
|
||||||
# TODO: workaround, find permanent fix
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def get_range(self, start_dt, end_dt):
|
def get_range(self, start_dt, end_dt):
|
||||||
return self._precalculated_series.loc[start_dt:end_dt]
|
return self._precalculated_series.loc[start_dt:end_dt]
|
||||||
@@ -161,6 +168,21 @@ class BenchmarkSource(object):
|
|||||||
ffill=True
|
ffill=True
|
||||||
)[asset]
|
)[asset]
|
||||||
|
|
||||||
|
return benchmark_series.pct_change()[1:]
|
||||||
|
elif self.emission_rate == '5-minute':
|
||||||
|
five_minutes = trading_calendar.five_minutes_for_sessions_in_range(
|
||||||
|
self.sessions[0], self.sessions[-1]
|
||||||
|
)
|
||||||
|
benchmark_series = data_portal.get_history_window(
|
||||||
|
[asset],
|
||||||
|
five_minutes[-1],
|
||||||
|
bar_count=len(five_minutes) + 1,
|
||||||
|
frequency='5m',
|
||||||
|
field='price',
|
||||||
|
data_frequency=self.emission_rate,
|
||||||
|
ffill=True,
|
||||||
|
)[asset]
|
||||||
|
|
||||||
return benchmark_series.pct_change()[1:]
|
return benchmark_series.pct_change()[1:]
|
||||||
else:
|
else:
|
||||||
start_date = asset.start_date
|
start_date = asset.start_date
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ from catalyst.protocol import (
|
|||||||
)
|
)
|
||||||
from catalyst.assets import Equity
|
from catalyst.assets import Equity
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
logger = Logger('Requests Source Logger')
|
||||||
|
|
||||||
logger = Logger('Requests Source Logger', level=LOG_LEVEL)
|
|
||||||
|
|
||||||
|
|
||||||
def roll_dts_to_midnight(dts, trading_day):
|
def roll_dts_to_midnight(dts, trading_day):
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols
|
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
symbols,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
context.i = -1
|
|
||||||
context.base_currency = 'btc'
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
lookback = 60 * 24 * 7 # (minutes, hours, days)
|
|
||||||
context.i += 1
|
|
||||||
if context.i < lookback:
|
|
||||||
return
|
|
||||||
|
|
||||||
today = context.blotter.current_dt.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# update universe everyday
|
|
||||||
new_day = 60 * 24
|
|
||||||
if not context.i % new_day:
|
|
||||||
context.universe = universe(context, today)
|
|
||||||
|
|
||||||
# get data every 30 minutes
|
|
||||||
minutes = 30
|
|
||||||
if not context.i % minutes and context.universe:
|
|
||||||
for coin in context.coins:
|
|
||||||
pair = str(coin.symbol)
|
|
||||||
|
|
||||||
# ohlcv data
|
|
||||||
open = data.history(coin, 'open', lookback,
|
|
||||||
'1m').ffill().bfill().resample(
|
|
||||||
'30T').first()
|
|
||||||
high = data.history(coin, 'high', lookback,
|
|
||||||
'1m').ffill().bfill().resample('30T').max()
|
|
||||||
low = data.history(coin, 'low', lookback,
|
|
||||||
'1m').ffill().bfill().resample('30T').min()
|
|
||||||
close = data.history(coin, 'price', lookback,
|
|
||||||
'1m').ffill().bfill().resample(
|
|
||||||
'30T').last()
|
|
||||||
volume = data.history(coin, 'volume', lookback,
|
|
||||||
'1m').ffill().bfill().resample(
|
|
||||||
'30T').sum()
|
|
||||||
|
|
||||||
print(today, pair, close[-1])
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
|
|
||||||
def analyze(context=None, results=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def universe(context, today):
|
|
||||||
json_symbols = get_exchange_symbols('poloniex')
|
|
||||||
poloniex_universe_df = pd.DataFrame.from_dict(
|
|
||||||
json_symbols).transpose().astype(str)
|
|
||||||
poloniex_universe_df['base_currency'] = poloniex_universe_df.apply(
|
|
||||||
lambda row: row.symbol.split('_')[1],
|
|
||||||
axis=1)
|
|
||||||
poloniex_universe_df['market_currency'] = poloniex_universe_df.apply(
|
|
||||||
lambda row: row.symbol.split('_')[0],
|
|
||||||
axis=1)
|
|
||||||
poloniex_universe_df = poloniex_universe_df[
|
|
||||||
poloniex_universe_df['base_currency'] == context.base_currency]
|
|
||||||
poloniex_universe_df = poloniex_universe_df[
|
|
||||||
poloniex_universe_df.symbol != 'gas_btc']
|
|
||||||
|
|
||||||
# Markets currently not working on Catalyst 0.3.1
|
|
||||||
# 2017-01-01
|
|
||||||
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'bcn_btc']
|
|
||||||
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'burst_btc']
|
|
||||||
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'dgb_btc']
|
|
||||||
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'doge_btc']
|
|
||||||
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'emc2_btc']
|
|
||||||
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'pink_btc']
|
|
||||||
# poloniex_universe_df = poloniex_universe_df[poloniex_universe_df.symbol != 'sc_btc']
|
|
||||||
print(poloniex_universe_df.head())
|
|
||||||
|
|
||||||
date = str(today).split(' ')[0]
|
|
||||||
|
|
||||||
poloniex_universe_df = poloniex_universe_df[
|
|
||||||
poloniex_universe_df.start_date < date]
|
|
||||||
context.coins = symbols(*poloniex_universe_df.symbol)
|
|
||||||
print(len(poloniex_universe_df))
|
|
||||||
return poloniex_universe_df.symbol.tolist()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
start_date = pd.to_datetime('2017-01-01', utc=True)
|
|
||||||
end_date = pd.to_datetime('2017-10-15', utc=True)
|
|
||||||
|
|
||||||
performance = run_algorithm(start=start_date, end=end_date,
|
|
||||||
capital_base=10000.0,
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='poloniex',
|
|
||||||
data_frequency='minute',
|
|
||||||
base_currency='btc',
|
|
||||||
live=False,
|
|
||||||
live_graph=False,
|
|
||||||
algo_namespace='test')
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
"""
|
|
||||||
Requires Catalyst version 0.3.0 or above
|
|
||||||
Tested on Catalyst version 0.3.3
|
|
||||||
|
|
||||||
These example aims to provide and easy way for users to learn how to collect data from the different exchanges.
|
|
||||||
You simply need to specify the exchange and the market that you want to focus on.
|
|
||||||
You will all see how to create a universe and filter it base on the exchange and the market you desire.
|
|
||||||
|
|
||||||
The example prints out the closing price of all the pairs for a given market-exchange every 30 minutes.
|
|
||||||
The example also contains the ohlcv minute data for the past seven days which could be used to create indicators
|
|
||||||
Use this as the backbone to create your own trading strategies.
|
|
||||||
|
|
||||||
Variables lookback date and date are used to ensure data for a coin existed on the lookback period specified.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from datetime import timedelta
|
|
||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols
|
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
symbols,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
context.i = -1 # counts the minutes
|
|
||||||
context.exchange = 'poloniex' # must match the exchange specified in run_algorithm
|
|
||||||
context.base_currency = 'btc' # must match the base currency specified in run_algorithm
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
lookback = 60 * 24 * 7 # (minutes, hours, days) of how far to lookback in the data history
|
|
||||||
context.i += 1
|
|
||||||
|
|
||||||
# current date formatted into a string
|
|
||||||
today = context.blotter.current_dt
|
|
||||||
date, time = today.strftime('%Y-%m-%d %H:%M:%S').split(' ')
|
|
||||||
lookback_date = today - timedelta(days=(
|
|
||||||
lookback / (60 * 24))) # subtract the amount of days specified in lookback
|
|
||||||
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[
|
|
||||||
0] # get only the date as a string
|
|
||||||
|
|
||||||
# update universe everyday
|
|
||||||
new_day = 60 * 24
|
|
||||||
if not context.i % new_day:
|
|
||||||
context.universe = universe(context, lookback_date, date)
|
|
||||||
|
|
||||||
# get data every 30 minutes
|
|
||||||
minutes = 30
|
|
||||||
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)
|
|
||||||
|
|
||||||
# 30 minute interval ohlcv data (the standard data required for candlestick or indicators/signals)
|
|
||||||
# 30T means 30 minutes re-sampling of one minute data. change to your desire time interval.
|
|
||||||
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 equivalent to current price
|
|
||||||
# displays the minute price for each pair every 30 minutes
|
|
||||||
print(
|
|
||||||
today, pair, opened[-1], high[-1], low[-1], close[-1], 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):
|
|
||||||
json_symbols = get_exchange_symbols(
|
|
||||||
context.exchange) # get all the pairs for the exchange
|
|
||||||
universe_df = pd.DataFrame.from_dict(json_symbols).transpose().astype(
|
|
||||||
str) # convert into a dataframe
|
|
||||||
universe_df['base_currency'] = universe_df.apply(
|
|
||||||
lambda row: row.symbol.split('_')[1],
|
|
||||||
axis=1)
|
|
||||||
universe_df['market_currency'] = universe_df.apply(
|
|
||||||
lambda row: row.symbol.split('_')[0],
|
|
||||||
axis=1)
|
|
||||||
# Filter all the exchange pairs to only the ones for a give base currency
|
|
||||||
universe_df = universe_df[
|
|
||||||
universe_df['base_currency'] == context.base_currency]
|
|
||||||
|
|
||||||
# Filter all the pairs to ensure that pair existed in the current date range
|
|
||||||
universe_df = universe_df[universe_df.start_date < lookback_date]
|
|
||||||
universe_df = universe_df[universe_df.end_daily >= current_date]
|
|
||||||
context.coins = symbols(
|
|
||||||
*universe_df.symbol) # convert all the pairs to symbols
|
|
||||||
return universe_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-01-08', utc=True)
|
|
||||||
end_date = pd.to_datetime('2017-11-13', utc=True)
|
|
||||||
|
|
||||||
performance = run_algorithm(start=start_date, end=end_date,
|
|
||||||
capital_base=10000.0,
|
|
||||||
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')
|
|
||||||
|
|
||||||
"""
|
|
||||||
Run in Terminal (inside catalyst environment):
|
|
||||||
python simple_universe.py
|
|
||||||
"""
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import talib
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.api import symbol
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
print('initializing')
|
|
||||||
context.asset = symbol('xcp_btc')
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
print('handling bar: {}'.format(data.current_dt))
|
|
||||||
|
|
||||||
price = data.current(context.asset, 'close')
|
|
||||||
print('got price {price}'.format(price=price))
|
|
||||||
|
|
||||||
try:
|
|
||||||
prices = data.history(
|
|
||||||
context.asset,
|
|
||||||
fields='close',
|
|
||||||
bar_count=1,
|
|
||||||
frequency='1D'
|
|
||||||
)
|
|
||||||
print('got {} price entries\n'.format(len(prices), prices))
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=1,
|
|
||||||
start=pd.to_datetime('2015-3-2', utc=True),
|
|
||||||
end=pd.to_datetime('2017-8-31', utc=True),
|
|
||||||
data_frequency='daily',
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=None,
|
|
||||||
exchange_name='poloniex',
|
|
||||||
algo_namespace='issue_55',
|
|
||||||
base_currency='btc'
|
|
||||||
)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import talib
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.api import symbol
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
print('initializing')
|
|
||||||
context.asset = symbol('btc_usdt')
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
print('handling bar: {}'.format(data.current_dt))
|
|
||||||
|
|
||||||
price = data.current(context.asset, 'close')
|
|
||||||
print('got price {price}'.format(price=price))
|
|
||||||
|
|
||||||
try:
|
|
||||||
prices = data.history(
|
|
||||||
context.asset,
|
|
||||||
fields='close',
|
|
||||||
bar_count=60,
|
|
||||||
frequency='1D'
|
|
||||||
)
|
|
||||||
print('got {} price entries\n'.format(len(prices), prices))
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=1,
|
|
||||||
start=pd.to_datetime('2016-2-11', utc=True),
|
|
||||||
end=pd.to_datetime('2017-8-31', utc=True),
|
|
||||||
data_frequency='daily',
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=None,
|
|
||||||
exchange_name='bittrex',
|
|
||||||
algo_namespace='issue_57',
|
|
||||||
base_currency='btc'
|
|
||||||
<<<<<<< HEAD
|
|
||||||
)
|
|
||||||
=======
|
|
||||||
)
|
|
||||||
>>>>>>> develop
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
from __future__ import division
|
|
||||||
import os
|
|
||||||
import pytz
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
from scipy.optimize import minimize
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from catalyst.api import record, symbol, 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='daily')
|
|
||||||
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 xrange(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')
|
|
||||||
|
|
||||||
|
|
||||||
# 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, )
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
from logbook import Logger, DEBUG
|
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
|
||||||
from catalyst.api import (schedule_function, order_target_percent, symbol,
|
|
||||||
date_rules, get_open_orders, cancel_order, record,
|
|
||||||
set_commission, set_slippage)
|
|
||||||
|
|
||||||
log = Logger('rodrigo_1', level=DEBUG)
|
|
||||||
"""
|
|
||||||
The initialize function sets any data or variables that
|
|
||||||
you'll use in your algorithm.
|
|
||||||
It's only called once at the beginning of your algorithm.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
# Select asset of interest
|
|
||||||
context.asset = symbol('BTC_USD')
|
|
||||||
|
|
||||||
# set_commission(TradingPairFeeSchedule(maker_fee=0.5, taker_fee=0.5))
|
|
||||||
# set_slippage(TradingPairFixedSlippage(spread=0.5))
|
|
||||||
# Set up a rebalance method to run every day
|
|
||||||
schedule_function(rebalance, date_rule=date_rules.every_day())
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
Rebalance function scheduled to run once per day.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def rebalance(context, data):
|
|
||||||
# To make market decisions, we're calculating the token's
|
|
||||||
# moving average for the last 5 days.
|
|
||||||
|
|
||||||
# We get the price history for the last 5 days.
|
|
||||||
price_history = data.history(context.asset, fields='price', bar_count=5,
|
|
||||||
frequency='1d')
|
|
||||||
|
|
||||||
# Then we take an average of those 5 days.
|
|
||||||
average_price = price_history.mean()
|
|
||||||
|
|
||||||
# We also get the coin's current price.
|
|
||||||
price = data.current(context.asset, 'price')
|
|
||||||
|
|
||||||
# Cancel any outstanding orders
|
|
||||||
orders = get_open_orders(context.asset) or []
|
|
||||||
for order in orders:
|
|
||||||
cancel_order(order)
|
|
||||||
|
|
||||||
# If our coin is currently listed on a major exchange
|
|
||||||
if data.can_trade(context.asset):
|
|
||||||
# If the current price is 1% above the 5-day average price,
|
|
||||||
# we open a long position. If the current price is below the
|
|
||||||
# average price, then we want to close our position to 0 shares.
|
|
||||||
if price > (1.01 * average_price):
|
|
||||||
# Place the buy order (positive means buy, negative means sell)
|
|
||||||
order_target_percent(context.asset, .99)
|
|
||||||
log.info("Buying %s" % (context.asset.symbol))
|
|
||||||
elif price < average_price:
|
|
||||||
# Sell all of our shares by setting the target position to zero
|
|
||||||
order_target_percent(context.asset, 0)
|
|
||||||
log.info("Selling %s" % (context.asset.symbol))
|
|
||||||
|
|
||||||
# Use the record() method to track up to five custom signals.
|
|
||||||
# Record Apple's current price and the average price over the last
|
|
||||||
# five days.
|
|
||||||
cash = context.portfolio.cash
|
|
||||||
leverage = context.account.leverage
|
|
||||||
|
|
||||||
record(price=price, average_price=average_price, cash=cash,
|
|
||||||
leverage=leverage)
|
|
||||||
|
|
||||||
|
|
||||||
def analyze(context=None, results=None):
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
# Plot the portfolio and asset data.
|
|
||||||
ax1 = plt.subplot(511)
|
|
||||||
results[['portfolio_value']].plot(ax=ax1)
|
|
||||||
ax1.set_ylabel('Portfolio Value (USD)')
|
|
||||||
|
|
||||||
ax2 = plt.subplot(512, sharex=ax1)
|
|
||||||
ax2.set_ylabel('{asset} (USD)'.format(asset=context.asset))
|
|
||||||
(results[[
|
|
||||||
'price',
|
|
||||||
]]).plot(ax=ax2)
|
|
||||||
|
|
||||||
trans = results.ix[[t != [] for t in results.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,
|
|
||||||
results.price[buys.index],
|
|
||||||
'^',
|
|
||||||
markersize=10,
|
|
||||||
color='g',
|
|
||||||
)
|
|
||||||
ax2.plot(
|
|
||||||
sells.index,
|
|
||||||
results.price[sells.index],
|
|
||||||
'v',
|
|
||||||
markersize=10,
|
|
||||||
color='r',
|
|
||||||
)
|
|
||||||
|
|
||||||
ax3 = plt.subplot(513, sharex=ax1)
|
|
||||||
results[['leverage']].plot(ax=ax3)
|
|
||||||
ax3.set_ylabel('Leverage ')
|
|
||||||
|
|
||||||
ax4 = plt.subplot(514, sharex=ax1)
|
|
||||||
results[['cash']].plot(ax=ax4)
|
|
||||||
ax4.set_ylabel('Cash (USD)')
|
|
||||||
|
|
||||||
results[[
|
|
||||||
'algorithm',
|
|
||||||
'benchmark',
|
|
||||||
]] = results[[
|
|
||||||
'algorithm_period_return',
|
|
||||||
'benchmark_period_return',
|
|
||||||
]]
|
|
||||||
|
|
||||||
ax5 = plt.subplot(515, sharex=ax1)
|
|
||||||
results[[
|
|
||||||
'algorithm',
|
|
||||||
'benchmark',
|
|
||||||
]].plot(ax=ax5)
|
|
||||||
ax5.set_ylabel('Percent Change')
|
|
||||||
|
|
||||||
plt.legend(loc=3)
|
|
||||||
|
|
||||||
# Show the plot.
|
|
||||||
plt.gcf().set_size_inches(18, 8)
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
|
||||||
capital_base=100000,
|
|
||||||
start=pd.to_datetime('2017-1-1', utc=True),
|
|
||||||
end=pd.to_datetime('2017-10-22', utc=True),
|
|
||||||
data_frequency='minute',
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=None,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='bitfinex',
|
|
||||||
algo_namespace='rodrigo_1',
|
|
||||||
base_currency='usd'
|
|
||||||
)
|
|
||||||
@@ -55,4 +55,4 @@ from .core import ( # noqa
|
|||||||
write_bcolz_minute_data,
|
write_bcolz_minute_data,
|
||||||
write_compressed,
|
write_compressed,
|
||||||
)
|
)
|
||||||
from .fixtures import CatalystTestCase # noqa
|
from .fixtures import ZiplineTestCase # noqa
|
||||||
|
|||||||
@@ -643,7 +643,7 @@ def create_data_portal_from_trade_history(asset_finder, trading_calendar,
|
|||||||
return DataPortal(
|
return DataPortal(
|
||||||
asset_finder, trading_calendar,
|
asset_finder, trading_calendar,
|
||||||
first_trading_day=equity_daily_reader.first_trading_day,
|
first_trading_day=equity_daily_reader.first_trading_day,
|
||||||
daily_reader=equity_daily_reader,
|
equity_daily_reader=equity_daily_reader,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
minutes = trading_calendar.minutes_in_range(
|
minutes = trading_calendar.minutes_in_range(
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ from catalyst.utils.paths import ensure_directory
|
|||||||
catalyst_dir = os.path.dirname(catalyst.__file__)
|
catalyst_dir = os.path.dirname(catalyst.__file__)
|
||||||
|
|
||||||
|
|
||||||
class CatalystTestCase(with_metaclass(FinalMeta, TestCase)):
|
class ZiplineTestCase(with_metaclass(FinalMeta, TestCase)):
|
||||||
"""
|
"""
|
||||||
Shared extensions to core unittest.TestCase.
|
Shared extensions to core unittest.TestCase.
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class CatalystTestCase(with_metaclass(FinalMeta, TestCase)):
|
|||||||
cls._base_init_fixtures_was_called = False
|
cls._base_init_fixtures_was_called = False
|
||||||
cls.init_class_fixtures()
|
cls.init_class_fixtures()
|
||||||
assert cls._base_init_fixtures_was_called, (
|
assert cls._base_init_fixtures_was_called, (
|
||||||
"CatalystTestCase.init_class_fixtures() was not called.\n"
|
"ZiplineTestCase.init_class_fixtures() was not called.\n"
|
||||||
"This probably means that you overrode init_class_fixtures"
|
"This probably means that you overrode init_class_fixtures"
|
||||||
" without calling super()."
|
" without calling super()."
|
||||||
)
|
)
|
||||||
@@ -170,7 +170,7 @@ class CatalystTestCase(with_metaclass(FinalMeta, TestCase)):
|
|||||||
self._init_instance_fixtures_was_called = False
|
self._init_instance_fixtures_was_called = False
|
||||||
self.init_instance_fixtures()
|
self.init_instance_fixtures()
|
||||||
assert self._init_instance_fixtures_was_called, (
|
assert self._init_instance_fixtures_was_called, (
|
||||||
"CatalystTestCase.init_instance_fixtures() was not"
|
"ZiplineTestCase.init_instance_fixtures() was not"
|
||||||
" called.\n"
|
" called.\n"
|
||||||
"This probably means that you overrode"
|
"This probably means that you overrode"
|
||||||
" init_instance_fixtures without calling super()."
|
" init_instance_fixtures without calling super()."
|
||||||
@@ -251,7 +251,7 @@ def alias(attr_name):
|
|||||||
|
|
||||||
class WithDefaultDateBounds(object):
|
class WithDefaultDateBounds(object):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin which makes it possible to synchronize date bounds
|
ZiplineTestCase mixin which makes it possible to synchronize date bounds
|
||||||
across fixtures.
|
across fixtures.
|
||||||
|
|
||||||
This fixture should always be the last fixture in bases of any fixture or
|
This fixture should always be the last fixture in bases of any fixture or
|
||||||
@@ -264,13 +264,13 @@ class WithDefaultDateBounds(object):
|
|||||||
The date bounds to be used for fixtures that want to have consistent
|
The date bounds to be used for fixtures that want to have consistent
|
||||||
dates.
|
dates.
|
||||||
"""
|
"""
|
||||||
START_DATE = pd.Timestamp('2016-01-03', tz='utc')
|
START_DATE = pd.Timestamp('2006-01-03', tz='utc')
|
||||||
END_DATE = pd.Timestamp('2016-12-29', tz='utc')
|
END_DATE = pd.Timestamp('2006-12-29', tz='utc')
|
||||||
|
|
||||||
|
|
||||||
class WithLogger(object):
|
class WithLogger(object):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.log_handler as an instance-level
|
ZiplineTestCase mixin providing cls.log_handler as an instance-level
|
||||||
fixture.
|
fixture.
|
||||||
|
|
||||||
After init_instance_fixtures has been called `self.log_handler` will be a
|
After init_instance_fixtures has been called `self.log_handler` will be a
|
||||||
@@ -295,7 +295,7 @@ class WithLogger(object):
|
|||||||
|
|
||||||
class WithAssetFinder(WithDefaultDateBounds):
|
class WithAssetFinder(WithDefaultDateBounds):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.asset_finder as a class-level fixture.
|
ZiplineTestCase mixin providing cls.asset_finder as a class-level fixture.
|
||||||
|
|
||||||
After init_class_fixtures has been called, `cls.asset_finder` is populated
|
After init_class_fixtures has been called, `cls.asset_finder` is populated
|
||||||
with an AssetFinder.
|
with an AssetFinder.
|
||||||
@@ -402,7 +402,7 @@ class WithAssetFinder(WithDefaultDateBounds):
|
|||||||
|
|
||||||
class WithTradingCalendars(object):
|
class WithTradingCalendars(object):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.trading_calendar,
|
ZiplineTestCase mixin providing cls.trading_calendar,
|
||||||
cls.all_trading_calendars, cls.trading_calendar_for_asset_type as a
|
cls.all_trading_calendars, cls.trading_calendar_for_asset_type as a
|
||||||
class-level fixture.
|
class-level fixture.
|
||||||
|
|
||||||
@@ -423,7 +423,7 @@ class WithTradingCalendars(object):
|
|||||||
with that asset type.
|
with that asset type.
|
||||||
"""
|
"""
|
||||||
TRADING_CALENDAR_STRS = ('NYSE',)
|
TRADING_CALENDAR_STRS = ('NYSE',)
|
||||||
TRADING_CALENDAR_FOR_ASSET_TYPE = {Equity: 'NYSE', Future: 'us_futures', }
|
TRADING_CALENDAR_FOR_ASSET_TYPE = {Equity: 'NYSE', Future: 'us_futures'}
|
||||||
TRADING_CALENDAR_FOR_EXCHANGE = {}
|
TRADING_CALENDAR_FOR_EXCHANGE = {}
|
||||||
# For backwards compatibility, exisitng tests and fixtures refer to
|
# For backwards compatibility, exisitng tests and fixtures refer to
|
||||||
# `trading_calendar` with the assumption that the value is the NYSE
|
# `trading_calendar` with the assumption that the value is the NYSE
|
||||||
@@ -460,7 +460,7 @@ class WithTradingEnvironment(WithAssetFinder,
|
|||||||
WithTradingCalendars,
|
WithTradingCalendars,
|
||||||
WithDefaultDateBounds):
|
WithDefaultDateBounds):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.env as a class-level fixture.
|
ZiplineTestCase mixin providing cls.env as a class-level fixture.
|
||||||
|
|
||||||
After ``init_class_fixtures`` has been called, `cls.env` is populated
|
After ``init_class_fixtures`` has been called, `cls.env` is populated
|
||||||
with a trading environment whose `asset_finder` is the result of
|
with a trading environment whose `asset_finder` is the result of
|
||||||
@@ -560,7 +560,7 @@ class WithTradingEnvironment(WithAssetFinder,
|
|||||||
|
|
||||||
class WithSimParams(WithTradingEnvironment):
|
class WithSimParams(WithTradingEnvironment):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.sim_params as a class level fixture.
|
ZiplineTestCase mixin providing cls.sim_params as a class level fixture.
|
||||||
|
|
||||||
The arguments used to construct the trading environment may be overridded
|
The arguments used to construct the trading environment may be overridded
|
||||||
by putting ``SIM_PARAMS_{argname}`` in the class dict except for the
|
by putting ``SIM_PARAMS_{argname}`` in the class dict except for the
|
||||||
@@ -615,7 +615,7 @@ class WithSimParams(WithTradingEnvironment):
|
|||||||
|
|
||||||
class WithTradingSessions(WithTradingCalendars, WithDefaultDateBounds):
|
class WithTradingSessions(WithTradingCalendars, WithDefaultDateBounds):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.trading_days, cls.all_trading_sessions
|
ZiplineTestCase mixin providing cls.trading_days, cls.all_trading_sessions
|
||||||
as a class-level fixture.
|
as a class-level fixture.
|
||||||
|
|
||||||
After init_class_fixtures has been called, `cls.all_trading_sessions`
|
After init_class_fixtures has been called, `cls.all_trading_sessions`
|
||||||
@@ -668,7 +668,7 @@ class WithTradingSessions(WithTradingCalendars, WithDefaultDateBounds):
|
|||||||
|
|
||||||
class WithTmpDir(object):
|
class WithTmpDir(object):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixing providing cls.tmpdir as a class-level fixture.
|
ZiplineTestCase mixing providing cls.tmpdir as a class-level fixture.
|
||||||
|
|
||||||
After init_class_fixtures has been called, `cls.tmpdir` is populated with
|
After init_class_fixtures has been called, `cls.tmpdir` is populated with
|
||||||
a `testfixtures.TempDirectory` object whose path is `cls.TMP_DIR_PATH`.
|
a `testfixtures.TempDirectory` object whose path is `cls.TMP_DIR_PATH`.
|
||||||
@@ -691,7 +691,7 @@ class WithTmpDir(object):
|
|||||||
|
|
||||||
class WithInstanceTmpDir(object):
|
class WithInstanceTmpDir(object):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixing providing self.tmpdir as an instance-level fixture.
|
ZiplineTestCase mixing providing self.tmpdir as an instance-level fixture.
|
||||||
|
|
||||||
After init_instance_fixtures has been called, `self.tmpdir` is populated
|
After init_instance_fixtures has been called, `self.tmpdir` is populated
|
||||||
with a `testfixtures.TempDirectory` object whose path is
|
with a `testfixtures.TempDirectory` object whose path is
|
||||||
@@ -714,7 +714,7 @@ class WithInstanceTmpDir(object):
|
|||||||
|
|
||||||
class WithEquityDailyBarData(WithTradingEnvironment):
|
class WithEquityDailyBarData(WithTradingEnvironment):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.make_equity_daily_bar_data.
|
ZiplineTestCase mixin providing cls.make_equity_daily_bar_data.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
@@ -810,7 +810,7 @@ class WithEquityDailyBarData(WithTradingEnvironment):
|
|||||||
|
|
||||||
class WithBcolzEquityDailyBarReader(WithEquityDailyBarData, WithTmpDir):
|
class WithBcolzEquityDailyBarReader(WithEquityDailyBarData, WithTmpDir):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.bcolz_daily_bar_path,
|
ZiplineTestCase mixin providing cls.bcolz_daily_bar_path,
|
||||||
cls.bcolz_daily_bar_ctable, and cls.bcolz_equity_daily_bar_reader
|
cls.bcolz_daily_bar_ctable, and cls.bcolz_equity_daily_bar_reader
|
||||||
class level fixtures.
|
class level fixtures.
|
||||||
|
|
||||||
@@ -895,7 +895,7 @@ class WithBcolzEquityDailyBarReader(WithEquityDailyBarData, WithTmpDir):
|
|||||||
|
|
||||||
class WithBcolzEquityDailyBarReaderFromCSVs(WithBcolzEquityDailyBarReader):
|
class WithBcolzEquityDailyBarReaderFromCSVs(WithBcolzEquityDailyBarReader):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin that provides
|
ZiplineTestCase mixin that provides
|
||||||
cls.bcolz_equity_daily_bar_reader from a mapping of sids to CSV
|
cls.bcolz_equity_daily_bar_reader from a mapping of sids to CSV
|
||||||
file paths.
|
file paths.
|
||||||
"""
|
"""
|
||||||
@@ -925,7 +925,7 @@ class _WithMinuteBarDataBase(WithTradingEnvironment):
|
|||||||
|
|
||||||
class WithEquityMinuteBarData(_WithMinuteBarDataBase):
|
class WithEquityMinuteBarData(_WithMinuteBarDataBase):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.equity_minute_bar_days.
|
ZiplineTestCase mixin providing cls.equity_minute_bar_days.
|
||||||
|
|
||||||
After init_class_fixtures has been called:
|
After init_class_fixtures has been called:
|
||||||
- `cls.equity_minute_bar_days` has the range over which data has been
|
- `cls.equity_minute_bar_days` has the range over which data has been
|
||||||
@@ -984,7 +984,7 @@ class WithEquityMinuteBarData(_WithMinuteBarDataBase):
|
|||||||
|
|
||||||
class WithFutureMinuteBarData(_WithMinuteBarDataBase):
|
class WithFutureMinuteBarData(_WithMinuteBarDataBase):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.future_minute_bar_days.
|
ZiplineTestCase mixin providing cls.future_minute_bar_days.
|
||||||
|
|
||||||
After init_class_fixtures has been called:
|
After init_class_fixtures has been called:
|
||||||
- `cls.future_minute_bar_days` has the range over which data has been
|
- `cls.future_minute_bar_days` has the range over which data has been
|
||||||
@@ -1044,7 +1044,7 @@ class WithFutureMinuteBarData(_WithMinuteBarDataBase):
|
|||||||
|
|
||||||
class WithBcolzEquityMinuteBarReader(WithEquityMinuteBarData, WithTmpDir):
|
class WithBcolzEquityMinuteBarReader(WithEquityMinuteBarData, WithTmpDir):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.bcolz_minute_bar_path,
|
ZiplineTestCase mixin providing cls.bcolz_minute_bar_path,
|
||||||
cls.bcolz_minute_bar_ctable, and cls.bcolz_equity_minute_bar_reader
|
cls.bcolz_minute_bar_ctable, and cls.bcolz_equity_minute_bar_reader
|
||||||
class level fixtures.
|
class level fixtures.
|
||||||
|
|
||||||
@@ -1103,7 +1103,7 @@ class WithBcolzEquityMinuteBarReader(WithEquityMinuteBarData, WithTmpDir):
|
|||||||
|
|
||||||
class WithBcolzFutureMinuteBarReader(WithFutureMinuteBarData, WithTmpDir):
|
class WithBcolzFutureMinuteBarReader(WithFutureMinuteBarData, WithTmpDir):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.bcolz_minute_bar_path,
|
ZiplineTestCase mixin providing cls.bcolz_minute_bar_path,
|
||||||
cls.bcolz_minute_bar_ctable, and cls.bcolz_equity_minute_bar_reader
|
cls.bcolz_minute_bar_ctable, and cls.bcolz_equity_minute_bar_reader
|
||||||
class level fixtures.
|
class level fixtures.
|
||||||
|
|
||||||
@@ -1227,7 +1227,7 @@ class WithConstantFutureMinuteBarData(WithFutureMinuteBarData):
|
|||||||
|
|
||||||
class WithAdjustmentReader(WithBcolzEquityDailyBarReader):
|
class WithAdjustmentReader(WithBcolzEquityDailyBarReader):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing cls.adjustment_reader as a class level
|
ZiplineTestCase mixin providing cls.adjustment_reader as a class level
|
||||||
fixture.
|
fixture.
|
||||||
|
|
||||||
After init_class_fixtures has been called, `cls.adjustment_reader` will be
|
After init_class_fixtures has been called, `cls.adjustment_reader` will be
|
||||||
@@ -1359,7 +1359,7 @@ class WithEquityPricingPipelineEngine(WithAdjustmentReader,
|
|||||||
|
|
||||||
class WithSeededRandomPipelineEngine(WithTradingSessions, WithAssetFinder):
|
class WithSeededRandomPipelineEngine(WithTradingSessions, WithAssetFinder):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing class-level fixtures for running pipelines
|
ZiplineTestCase mixin providing class-level fixtures for running pipelines
|
||||||
against deterministically-generated random data.
|
against deterministically-generated random data.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
@@ -1434,7 +1434,7 @@ class WithDataPortal(WithAdjustmentReader,
|
|||||||
WithBcolzEquityMinuteBarReader,
|
WithBcolzEquityMinuteBarReader,
|
||||||
WithBcolzFutureMinuteBarReader):
|
WithBcolzFutureMinuteBarReader):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin providing self.data_portal as an instance level
|
ZiplineTestCase mixin providing self.data_portal as an instance level
|
||||||
fixture.
|
fixture.
|
||||||
|
|
||||||
After init_instance_fixtures has been called, `self.data_portal` will be
|
After init_instance_fixtures has been called, `self.data_portal` will be
|
||||||
@@ -1485,12 +1485,12 @@ class WithDataPortal(WithAdjustmentReader,
|
|||||||
self.env.asset_finder,
|
self.env.asset_finder,
|
||||||
self.trading_calendar,
|
self.trading_calendar,
|
||||||
first_trading_day=self.DATA_PORTAL_FIRST_TRADING_DAY,
|
first_trading_day=self.DATA_PORTAL_FIRST_TRADING_DAY,
|
||||||
daily_reader=(
|
equity_daily_reader=(
|
||||||
self.bcolz_equity_daily_bar_reader
|
self.bcolz_equity_daily_bar_reader
|
||||||
if self.DATA_PORTAL_USE_DAILY_DATA else
|
if self.DATA_PORTAL_USE_DAILY_DATA else
|
||||||
None
|
None
|
||||||
),
|
),
|
||||||
minute_reader=(
|
equity_minute_reader=(
|
||||||
self.bcolz_equity_minute_bar_reader
|
self.bcolz_equity_minute_bar_reader
|
||||||
if self.DATA_PORTAL_USE_MINUTE_DATA else
|
if self.DATA_PORTAL_USE_MINUTE_DATA else
|
||||||
None
|
None
|
||||||
@@ -1526,7 +1526,7 @@ class WithDataPortal(WithAdjustmentReader,
|
|||||||
|
|
||||||
class WithResponses(object):
|
class WithResponses(object):
|
||||||
"""
|
"""
|
||||||
CatalystTestCase mixin that provides self.responses as an instance
|
ZiplineTestCase mixin that provides self.responses as an instance
|
||||||
fixture.
|
fixture.
|
||||||
|
|
||||||
After init_instance_fixtures has been called, `self.responses` will be
|
After init_instance_fixtures has been called, `self.responses` will be
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ class OpenExchangeCalendar(TradingCalendar):
|
|||||||
return DateOffset(days=1)
|
return DateOffset(days=1)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(OpenExchangeCalendar, self).__init__(start=Timestamp('2015-3-1', tz='UTC'), **kwargs)
|
super(OpenExchangeCalendar, self).__init__(start=Timestamp('2015-03-01', tz='UTC'), **kwargs)
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ class TradingCalendar(with_metaclass(ABCMeta)):
|
|||||||
self._trading_minutes_nanos = self.all_minutes.values.\
|
self._trading_minutes_nanos = self.all_minutes.values.\
|
||||||
astype(np.int64)
|
astype(np.int64)
|
||||||
|
|
||||||
|
self._trading_five_minutes_nanos = self.all_five_minutes.values.\
|
||||||
|
astype(np.int64)
|
||||||
|
|
||||||
self.first_trading_session = _all_days[0]
|
self.first_trading_session = _all_days[0]
|
||||||
self.last_trading_session = _all_days[-1]
|
self.last_trading_session = _all_days[-1]
|
||||||
|
|
||||||
@@ -179,6 +182,18 @@ class TradingCalendar(with_metaclass(ABCMeta)):
|
|||||||
"""
|
"""
|
||||||
return int(self._minutes_per_session[start_session:end_session].sum())
|
return int(self._minutes_per_session[start_session:end_session].sum())
|
||||||
|
|
||||||
|
@lazyval
|
||||||
|
def _five_minutes_per_session(self):
|
||||||
|
diff = self.schedule.market_close - self.schedule.market_open
|
||||||
|
diff = diff.astype('timedelta64[m]')
|
||||||
|
return (diff + 1) // 5
|
||||||
|
|
||||||
|
def five_minutes_count_for_sessions_in_range(self,
|
||||||
|
start_session,
|
||||||
|
end_session):
|
||||||
|
five_mins = self._five_minutes_per_session[start_session:end_session]
|
||||||
|
return int(five_mins.sum())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def regular_holidays(self):
|
def regular_holidays(self):
|
||||||
"""
|
"""
|
||||||
@@ -371,6 +386,10 @@ class TradingCalendar(with_metaclass(ABCMeta)):
|
|||||||
idx = next_divider_idx(self._trading_minutes_nanos, dt.value)
|
idx = next_divider_idx(self._trading_minutes_nanos, dt.value)
|
||||||
return self.all_minutes[idx]
|
return self.all_minutes[idx]
|
||||||
|
|
||||||
|
def next_five_minute(self, dt):
|
||||||
|
idx = next_divider_idx(self._trading_five_minutes_nanos, dt.values)
|
||||||
|
return self.all_five_mintutes[idx]
|
||||||
|
|
||||||
def previous_minute(self, dt):
|
def previous_minute(self, dt):
|
||||||
"""
|
"""
|
||||||
Given a dt, return the previous exchange minute.
|
Given a dt, return the previous exchange minute.
|
||||||
@@ -465,6 +484,12 @@ class TradingCalendar(with_metaclass(ABCMeta)):
|
|||||||
end_minute=self.schedule.at[session_label, 'market_close'],
|
end_minute=self.schedule.at[session_label, 'market_close'],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def five_minutes_for_session(self, session_label):
|
||||||
|
return self.five_minutes_in_range(
|
||||||
|
start_five_minute=self.schedule.at[session_label, 'market_open'],
|
||||||
|
end_five_minute=self.schedule.at[session_label, 'market_close'],
|
||||||
|
)
|
||||||
|
|
||||||
def minutes_window(self, start_dt, count):
|
def minutes_window(self, start_dt, count):
|
||||||
start_dt_nanos = start_dt.value
|
start_dt_nanos = start_dt.value
|
||||||
all_minutes_nanos = self._trading_minutes_nanos
|
all_minutes_nanos = self._trading_minutes_nanos
|
||||||
@@ -566,6 +591,20 @@ class TradingCalendar(with_metaclass(ABCMeta)):
|
|||||||
|
|
||||||
return abs(end_idx - start_idx)
|
return abs(end_idx - start_idx)
|
||||||
|
|
||||||
|
def five_minutes_in_range(self, start_five_minute, end_five_minute):
|
||||||
|
start_idx = searchsorted(self._trading_five_minutes_nanos,
|
||||||
|
start_five_minute.value)
|
||||||
|
|
||||||
|
end_idx = searchsorted(self._trading_five_minutes_nanos,
|
||||||
|
end_five_minute.value)
|
||||||
|
|
||||||
|
if end_five_minute.value == self._trading_five_minutes_nanos[end_idx]:
|
||||||
|
# if the end minute is a market minute, increase by 1
|
||||||
|
end_idx += 1
|
||||||
|
|
||||||
|
return self.all_five_minutes[start_idx:end_idx]
|
||||||
|
|
||||||
|
|
||||||
def minutes_in_range(self, start_minute, end_minute):
|
def minutes_in_range(self, start_minute, end_minute):
|
||||||
"""
|
"""
|
||||||
Given start and end minutes, return all the calendar minutes
|
Given start and end minutes, return all the calendar minutes
|
||||||
@@ -623,6 +662,15 @@ class TradingCalendar(with_metaclass(ABCMeta)):
|
|||||||
|
|
||||||
return self.minutes_in_range(first_minute, last_minute)
|
return self.minutes_in_range(first_minute, last_minute)
|
||||||
|
|
||||||
|
def five_minutes_for_sessions_in_range(self,
|
||||||
|
start_session_label,
|
||||||
|
end_session_label):
|
||||||
|
|
||||||
|
first_minute, _ = self.open_and_close_for_session(start_session_label)
|
||||||
|
_, last_minute = self.open_and_close_for_session(end_session_label)
|
||||||
|
|
||||||
|
return self.five_minutes_in_range(first_minute, last_minute)
|
||||||
|
|
||||||
def open_and_close_for_session(self, session_label):
|
def open_and_close_for_session(self, session_label):
|
||||||
"""
|
"""
|
||||||
Returns a tuple of timestamps of the open and close of the session
|
Returns a tuple of timestamps of the open and close of the session
|
||||||
@@ -729,6 +777,13 @@ class TradingCalendar(with_metaclass(ABCMeta)):
|
|||||||
|
|
||||||
return DatetimeIndex(all_minutes).tz_localize("UTC")
|
return DatetimeIndex(all_minutes).tz_localize("UTC")
|
||||||
|
|
||||||
|
@lazyval
|
||||||
|
def all_five_minutes(self):
|
||||||
|
"""
|
||||||
|
Returns a DatetimeIndex representing all the five minutes in this calendar.
|
||||||
|
"""
|
||||||
|
return self._all_minutes_with_interval(5)
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
def all_minutes(self):
|
def all_minutes(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -602,6 +602,7 @@ class date_rules(object):
|
|||||||
class time_rules(object):
|
class time_rules(object):
|
||||||
market_open = AfterOpen
|
market_open = AfterOpen
|
||||||
market_close = BeforeClose
|
market_close = BeforeClose
|
||||||
|
every_5_minutes = Always
|
||||||
every_minute = Always
|
every_minute = Always
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user