mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-22 12:40:30 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
803823eac0 | ||
|
|
5a18e09730 | ||
|
|
dd41f8c006 | ||
|
|
7a2a4817fe | ||
|
|
105522e5ab | ||
|
|
a52e201f86 | ||
|
|
697ff54125 | ||
|
|
4bcd34bd78 | ||
|
|
64c52c7a3c | ||
|
|
1c143eb9ea | ||
|
|
1da4ccfb8c | ||
|
|
a61b22b821 | ||
|
|
d07e0edd88 | ||
|
|
c2821ab77b | ||
|
|
1696db930d | ||
|
|
9397b3fd5a | ||
|
|
2bb11db412 | ||
|
|
c2f3e00d99 | ||
|
|
292fe66d3f | ||
|
|
ba46015bae | ||
|
|
12d5915c8e | ||
|
|
c6fe45371c | ||
|
|
968e70b69b | ||
|
|
6a7c47f3a9 | ||
|
|
7daf295e63 | ||
|
|
7dddc0a85f | ||
|
|
32523d474d | ||
|
|
841acf0203 | ||
|
|
b4ab1a5375 | ||
|
|
3ec9853b75 | ||
|
|
c1d140a831 | ||
|
|
02dc4d6a30 | ||
|
|
0d366a350d | ||
|
|
1e8b0c36a1 | ||
|
|
0af592a5f4 | ||
|
|
86b2a5c772 | ||
|
|
9cfd50dc4f | ||
|
|
698b19c8fa | ||
|
|
5d4bc99097 | ||
|
|
cfb3f1ca42 | ||
|
|
ee1605a5e6 | ||
|
|
f3dca74e87 | ||
|
|
d57b79427b | ||
|
|
8a89c0c53f | ||
|
|
c260e188b0 | ||
|
|
230b9c17eb | ||
|
|
2a8b5cf911 | ||
|
|
3fa88a3e56 | ||
|
|
64532c3d08 | ||
|
|
5f86ab659e | ||
|
|
df14a94918 | ||
|
|
e087e48088 | ||
|
|
5110b37a82 | ||
|
|
a2bb231424 | ||
|
|
e939f742a8 | ||
|
|
9093be748e | ||
|
|
e23a7e67a0 | ||
|
|
273b4fb7a7 | ||
|
|
0b2684d532 | ||
|
|
224192a1ee | ||
|
|
2d7202ac81 | ||
|
|
8d95428fa6 | ||
|
|
d678376d8d | ||
|
|
9b5fa83da3 | ||
|
|
0f1c3e1ace | ||
|
|
f3cb610748 | ||
|
|
1e6316d414 | ||
|
|
df51cbe21b | ||
|
|
dce31b212b | ||
|
|
00269d3dfb | ||
|
|
648be3969a | ||
|
|
a54325fdcf | ||
|
|
b64e5929b4 | ||
|
|
631cbcd352 | ||
|
|
24c5a5bd13 | ||
|
|
1103947af0 | ||
|
|
061de3c12f | ||
|
|
207887a28d | ||
|
|
dc53f973e4 | ||
|
|
9a80a488cd | ||
|
|
a85b6c798a | ||
|
|
9229809b05 | ||
|
|
f81cf6b600 | ||
|
|
ba4ffc7272 | ||
|
|
12695474e3 | ||
|
|
9d1dd5829d | ||
|
|
d4148891fc | ||
|
|
c9c16f54b1 | ||
|
|
7da72fe9cb | ||
|
|
515c6e13f0 | ||
|
|
5abdc063eb | ||
|
|
360e1adc22 | ||
|
|
8c6ac53a05 | ||
|
|
b636edb32f | ||
|
|
d02c6d8ce9 | ||
|
|
88f6557aaf | ||
|
|
b476024612 | ||
|
|
a3808c31ef | ||
|
|
5d251f6f9a | ||
|
|
117332d0b4 | ||
|
|
2bbc0c00cc | ||
|
|
5e4ad9b338 | ||
|
|
a9a422c892 | ||
|
|
5b6bbacab0 | ||
|
|
35677c553c | ||
|
|
df357d2327 | ||
|
|
e6ff7ee4fc | ||
|
|
30eea4b8f7 | ||
|
|
7ad047a432 | ||
|
|
e291a260b2 | ||
|
|
9a9e66b43d | ||
|
|
1c3deb648a | ||
|
|
b39311de85 | ||
|
|
5417a0cdcf | ||
|
|
6a4ea43d27 | ||
|
|
7fc1ade46c | ||
|
|
635fc80ef2 | ||
|
|
c59d805717 | ||
|
|
3394614ecf | ||
|
|
06c8ab9c37 | ||
|
|
6a0d0a0422 | ||
|
|
9470771561 | ||
|
|
8132e1f5ea | ||
|
|
0b28bf0e96 | ||
|
|
13f023d364 | ||
|
|
eaefe4a908 | ||
|
|
f47b657c6f | ||
|
|
800a2efa50 | ||
|
|
7465e8e432 | ||
|
|
86ab3804e5 | ||
|
|
b749d47a61 | ||
|
|
032c7fd16b | ||
|
|
b9579ab4b4 | ||
|
|
c7632b57a6 | ||
|
|
da17e66961 | ||
|
|
cd0157347f | ||
|
|
76a8362e3d | ||
|
|
c8cc2edd36 | ||
|
|
5f8016c67e | ||
|
|
3d88d6a2c7 | ||
|
|
9c3a9e233b | ||
|
|
c43509c28e | ||
|
|
0e0bfc82b5 | ||
|
|
2f660db511 | ||
|
|
fdc5a30060 | ||
|
|
bb1d96ed5d | ||
|
|
59501905ab | ||
|
|
cb870422c3 | ||
|
|
2b85732e36 | ||
|
|
284c749bb5 | ||
|
|
d7f5e73f84 | ||
|
|
cde69da173 | ||
|
|
bcc75f6b00 | ||
|
|
f179381b64 | ||
|
|
10ba53b897 | ||
|
|
1cfe3b1bb2 | ||
|
|
268ff9c826 | ||
|
|
7eb184d946 | ||
|
|
1cc34a1485 | ||
|
|
aa2f2f3627 | ||
|
|
7e373e2f9c | ||
|
|
942e6f263c | ||
|
|
2e6d7d28ba | ||
|
|
3a823ea457 | ||
|
|
4daba6cfb4 | ||
|
|
fa018e2e0c | ||
|
|
315d25f7c0 | ||
|
|
cc7ffada96 | ||
|
|
5394c1bc91 | ||
|
|
b230b73829 | ||
|
|
930a68ab4a | ||
|
|
4e833981e4 | ||
|
|
2ea402ff10 | ||
|
|
da6b024edc | ||
|
|
565e9a3cea | ||
|
|
3c10d19a7e | ||
|
|
cf96e047cd | ||
|
|
6f6a8e1272 | ||
|
|
c2a02e7074 | ||
|
|
7d2cf97fbf | ||
|
|
195469897c | ||
|
|
c7b422d465 | ||
|
|
2dbace37bb | ||
|
|
2e903fd42c | ||
|
|
47a104b29c | ||
|
|
d248581523 | ||
|
|
48f6300e08 | ||
|
|
f7a143cb78 | ||
|
|
2f7cd97852 | ||
|
|
73eca75ed9 | ||
|
|
2ade2989e8 | ||
|
|
b1d5acf2ad | ||
|
|
5d5ec6b9be | ||
|
|
1b84023c5d | ||
|
|
97f3329c1b | ||
|
|
493fc95a20 | ||
|
|
bdeb344999 | ||
|
|
52e1de954f | ||
|
|
7b9eafef4e | ||
|
|
f918fc97bc | ||
|
|
18e19bb1ae | ||
|
|
f72074876d | ||
|
|
fadd4abe5a | ||
|
|
5fd4ca33d3 | ||
|
|
653f4c2a5a | ||
|
|
3804af3813 | ||
|
|
f56abcfc3e | ||
|
|
cb6432c395 | ||
|
|
946d24bd7a | ||
|
|
b1a247df6a | ||
|
|
2c91decc1b | ||
|
|
09f27e5880 | ||
|
|
2dd8f54148 | ||
|
|
2d41f124f0 | ||
|
|
619eb3cfa4 | ||
|
|
331a31b25a | ||
|
|
6f57660944 | ||
|
|
1a97111ceb | ||
|
|
2502c9a2bb | ||
|
|
675957b197 | ||
|
|
51172759d3 | ||
|
|
6097128d5c | ||
|
|
5ccdd54274 | ||
|
|
b3dcb7a9ad | ||
|
|
8a6d0d7ca0 | ||
|
|
e5f7c63ebd | ||
|
|
2e46323a9e | ||
|
|
874a4bb682 | ||
|
|
339fa21c35 | ||
|
|
1c5822bce9 | ||
|
|
b785c10036 | ||
|
|
b69e78b27d | ||
|
|
6486744c66 | ||
|
|
6f8fbc2b82 | ||
|
|
521484355a | ||
|
|
7147bfc51f | ||
|
|
b357a0656a | ||
|
|
86f892eade | ||
|
|
188a4a3f3d | ||
|
|
fb32e1ce5d | ||
|
|
733f2c3433 | ||
|
|
74fd4a6a0f | ||
|
|
1a4dfe8abb | ||
|
|
6c17bbf0c9 | ||
|
|
4649b31d89 | ||
|
|
ead6769ea2 | ||
|
|
d21cc36bef | ||
|
|
105fee0fb9 | ||
|
|
a4389ffea4 | ||
|
|
989ffc57f1 | ||
|
|
9bdd8aba48 | ||
|
|
dadf7bd108 | ||
|
|
e98d10c41b | ||
|
|
1263fdd995 | ||
|
|
1732b4a985 | ||
|
|
403f951c77 | ||
|
|
bdbaad1c91 | ||
|
|
c52653c84e | ||
|
|
93f4d31399 | ||
|
|
c658d15fcb | ||
|
|
e1c2f40ab9 | ||
|
|
1a87d5a0c0 | ||
|
|
1dcfd169fa | ||
|
|
1c7fd19652 | ||
|
|
c67cbedfbf | ||
|
|
73378962aa | ||
|
|
4895bef392 | ||
|
|
3f9b44f3e4 | ||
|
|
c24918e2c8 | ||
|
|
01aeb88e8f | ||
|
|
c33bab673f | ||
|
|
d3e33c44bf | ||
|
|
de409efd3e | ||
|
|
83af12c52c | ||
|
|
8811aa669a | ||
|
|
4f80ebee57 | ||
|
|
403be97143 | ||
|
|
16cdc196b0 | ||
|
|
1d79e88312 | ||
|
|
3335ae0ea9 | ||
|
|
a1cf00e6fe | ||
|
|
0cc9d839d0 | ||
|
|
a004a01cdb | ||
|
|
04fc7855d5 | ||
|
|
50f075792c | ||
|
|
14f8c25c89 | ||
|
|
874968bbbb | ||
|
|
751608c8ab | ||
|
|
8b141a0c28 | ||
|
|
e11ecf9d78 | ||
|
|
b45339692f | ||
|
|
336f062794 | ||
|
|
6d8b8307a1 | ||
|
|
3b681d197d | ||
|
|
15fa98420d | ||
|
|
9dfefec13c | ||
|
|
6bfe0eecd2 | ||
|
|
3362dbf95c | ||
|
|
1d0faf693d | ||
|
|
87ecf6114d | ||
|
|
2c2c861a8f | ||
|
|
fef08d1433 | ||
|
|
cf20f78e55 | ||
|
|
5d1bdee4a6 | ||
|
|
f60abcd636 | ||
|
|
4798fc75fb | ||
|
|
d6996b1e93 | ||
|
|
bc65c10fc6 | ||
|
|
27f20a090a | ||
|
|
75c2753b98 | ||
|
|
a6b873508b | ||
|
|
daf3c4d285 | ||
|
|
8cabb33372 | ||
|
|
ddecd6bb48 | ||
|
|
2f8768bb06 | ||
|
|
df8ba90236 | ||
|
|
7f602d7fcc | ||
|
|
6baf4c2122 | ||
|
|
1f56325895 | ||
|
|
7335810cc2 | ||
|
|
10a5b5412e | ||
|
|
c5cbbce8e1 | ||
|
|
7359cdc48f | ||
|
|
4e2d092123 | ||
|
|
42566ca92c | ||
|
|
09bf875d6c | ||
|
|
b354837b83 | ||
|
|
a1bc174740 | ||
|
|
ea27346876 | ||
|
|
81bd2d84f0 | ||
|
|
06f48cf158 | ||
|
|
05a69cfc92 | ||
|
|
36c2564bb0 | ||
|
|
91e71c5e38 | ||
|
|
3b655d466e | ||
|
|
68546a0d8d | ||
|
|
b70ff3a740 | ||
|
|
18bfaff7c9 | ||
|
|
555b7e95b5 | ||
|
|
1d6336afda | ||
|
|
394777217d | ||
|
|
e761433d06 | ||
|
|
6fddb92563 | ||
|
|
4a4277d9d1 | ||
|
|
3361b09ac2 | ||
|
|
5a345a3abb | ||
|
|
01eefd67e0 | ||
|
|
d124125258 | ||
|
|
72e07e242f | ||
|
|
c3897cfa5a | ||
|
|
e5a137f205 | ||
|
|
48143d3212 | ||
|
|
ff0dc5cff9 | ||
|
|
41d9bbca1b | ||
|
|
3e2a8dd78b | ||
|
|
7e280aeb5c | ||
|
|
36881b03e2 | ||
|
|
8850657f26 | ||
|
|
6e6c62533b | ||
|
|
6a98a937dd | ||
|
|
c4900af088 | ||
|
|
6e3017010f | ||
|
|
7247b761d5 | ||
|
|
a58e3522a9 | ||
|
|
ad95369028 |
+3
-1
@@ -1 +1,3 @@
|
|||||||
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>`_.
|
All the documentation for `Catalyst <https://github.com/enigmampc/catalyst>`_
|
||||||
|
can be found in the
|
||||||
|
`documentation website <https://enigmampc.github.io/catalyst>`_.
|
||||||
+330
-46
@@ -8,6 +8,9 @@ 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
|
||||||
|
|
||||||
@@ -27,17 +30,19 @@ except NameError:
|
|||||||
@click.option(
|
@click.option(
|
||||||
'--strict-extensions/--non-strict-extensions',
|
'--strict-extensions/--non-strict-extensions',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help='If --strict-extensions is passed then catalyst will not run if it'
|
help='If --strict-extensions is passed then catalyst will not run '
|
||||||
' cannot load all of the specified extensions. If this is not passed or'
|
'if it cannot load all of the specified extensions. If this is '
|
||||||
' --non-strict-extensions is passed then the failure will be logged but'
|
'not passed or --non-strict-extensions is passed then the '
|
||||||
' execution will continue.',
|
'failure will be logged but execution will continue.',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--default-extension/--no-default-extension',
|
'--default-extension/--no-default-extension',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=True,
|
default=True,
|
||||||
help="Don't load the default catalyst extension.py file in $ZIPLINE_HOME.",
|
help="Don't load the default catalyst extension.py file "
|
||||||
|
"in $CATALYST_HOME.",
|
||||||
)
|
)
|
||||||
|
@click.version_option()
|
||||||
def main(extension, strict_extensions, default_extension):
|
def main(extension, strict_extensions, default_extension):
|
||||||
"""Top level catalyst entry point.
|
"""Top level catalyst entry point.
|
||||||
"""
|
"""
|
||||||
@@ -120,13 +125,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 any python"
|
" the algotext. For example '-Dname=value'. The value may be"
|
||||||
" expression. These are evaluated in order so they may refer to previously"
|
" any python expression. These are evaluated in order so they"
|
||||||
" defined names.",
|
" may refer to previously defined names.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--data-frequency',
|
'--data-frequency',
|
||||||
type=click.Choice({'daily', '5-minute', 'minute'}),
|
type=click.Choice({'daily', '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.',
|
||||||
@@ -134,7 +139,6 @@ def ipython_only(option):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'--capital-base',
|
'--capital-base',
|
||||||
type=float,
|
type=float,
|
||||||
default=10e6,
|
|
||||||
show_default=True,
|
show_default=True,
|
||||||
help='The starting capital for the simulation.',
|
help='The starting capital for the simulation.',
|
||||||
)
|
)
|
||||||
@@ -172,8 +176,8 @@ def ipython_only(option):
|
|||||||
default='-',
|
default='-',
|
||||||
metavar='FILENAME',
|
metavar='FILENAME',
|
||||||
show_default=True,
|
show_default=True,
|
||||||
help="The location to write the perf data. If this is '-' the perf will"
|
help="The location to write the perf data. If this is '-' the perf"
|
||||||
" be written to stdout.",
|
" will be written to stdout.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--print-algo/--no-print-algo',
|
'--print-algo/--no-print-algo',
|
||||||
@@ -187,17 +191,12 @@ def ipython_only(option):
|
|||||||
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'}),
|
type=click.Choice({'bitfinex', 'bittrex', 'poloniex'}),
|
||||||
help='The name of the targeted exchange (supported: bitfinex, bittrex).',
|
help='The name of the targeted exchange (supported: bitfinex,'
|
||||||
|
' bittrex, poloniex).',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-n',
|
'-n',
|
||||||
@@ -224,42 +223,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 (algotext is not None) == (algofile is not None):
|
||||||
if exchange_name is None:
|
ctx.fail(
|
||||||
ctx.fail("must specify an exchange name '-x' in live execution "
|
"must specify exactly one of '-f' / '--algofile' or"
|
||||||
"mode '--live'")
|
" '-t' / '--algotext'",
|
||||||
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 base currency '-c' in live "
|
|
||||||
"execution mode '--live'")
|
|
||||||
else:
|
|
||||||
# check that the start and end dates are passed correctly
|
# check that the start and end dates are passed correctly
|
||||||
if start is None and end is None:
|
if start is None and end is None:
|
||||||
# check both at the same time to avoid the case where a user
|
# 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
|
# does not pass either of these and then passes the first only
|
||||||
# to be told they need to pass the second argument also
|
# to be told they need to pass the second argument also
|
||||||
ctx.fail(
|
ctx.fail(
|
||||||
"must specify dates with '-s' / '--start' and '-e' / '--end'",
|
"must specify dates with '-s' / '--start' and '-e' / '--end'"
|
||||||
|
" in backtest mode",
|
||||||
)
|
)
|
||||||
if start is None:
|
if start is None:
|
||||||
ctx.fail("must specify a start date with '-s' / '--start'")
|
ctx.fail("must specify a start date with '-s' / '--start'"
|
||||||
|
" in backtest mode")
|
||||||
if end is None:
|
if end is None:
|
||||||
ctx.fail("must specify an end date with '-e' / '--end'")
|
ctx.fail("must specify an end date with '-e' / '--end'"
|
||||||
|
" in backtest mode")
|
||||||
|
|
||||||
if (algotext is not None) == (algofile is not None):
|
if exchange_name is None:
|
||||||
ctx.fail(
|
ctx.fail("must specify an exchange name '-x'")
|
||||||
"must specify exactly one of '-f' / '--algofile' or"
|
|
||||||
" '-t' / '--algotext'",
|
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,
|
||||||
@@ -280,10 +280,11 @@ 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=live,
|
live=False,
|
||||||
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 == '-':
|
||||||
@@ -327,15 +328,288 @@ 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',
|
||||||
show_default=True,
|
default=None,
|
||||||
|
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',
|
||||||
@@ -354,9 +628,12 @@ def catalyst_magic(line, cell=None):
|
|||||||
default=True,
|
default=True,
|
||||||
help='Print progress information to the terminal.'
|
help='Print progress information to the terminal.'
|
||||||
)
|
)
|
||||||
def ingest(bundle, compile_locally, assets_version, show_progress):
|
@click.pass_context
|
||||||
|
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,
|
||||||
@@ -376,6 +653,13 @@ def ingest(bundle, compile_locally, assets_version, show_progress):
|
|||||||
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',
|
||||||
@@ -399,7 +683,7 @@ def ingest(bundle, compile_locally, assets_version, show_progress):
|
|||||||
' This may not be passed with -e / --before or -a / --after',
|
' This may not be passed with -e / --before or -a / --after',
|
||||||
)
|
)
|
||||||
def clean(bundle, before, after, keep_last):
|
def clean(bundle, before, after, keep_last):
|
||||||
"""Clean up data downloaded with the ingest command.
|
"""Clean up bundles from 'ingest'.
|
||||||
"""
|
"""
|
||||||
bundles_module.clean(
|
bundles_module.clean(
|
||||||
bundle,
|
bundle,
|
||||||
|
|||||||
+20
-48
@@ -125,6 +125,7 @@ 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
|
||||||
@@ -133,15 +134,13 @@ 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 (
|
from catalyst.gens.sim_engine import MinuteSimulationClock
|
||||||
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("ZiplineLog")
|
log = logbook.Logger("CatalystLog", level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class TradingAlgorithm(object):
|
class TradingAlgorithm(object):
|
||||||
@@ -173,7 +172,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', '5-minute', 'minute'}, optional
|
data_frequency : {'daily', '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
|
||||||
@@ -226,7 +225,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', '5-minute', 'minute'}
|
data_frequency : {'daily', '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.
|
||||||
@@ -434,8 +433,6 @@ 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:
|
||||||
@@ -467,7 +464,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 in ('minute', '5-minute') else ExitStack():
|
self.data_frequency == '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
|
||||||
@@ -523,11 +520,10 @@ 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 in set(('minute', '5-minute')):
|
if self.sim_params.data_frequency == 'minute':
|
||||||
market_opens = trading_o_and_c['market_open']
|
market_opens = trading_o_and_c['market_open']
|
||||||
|
|
||||||
minutely_emission = self.sim_params.emission_rate in \
|
minutely_emission = self.sim_params.emission_rate == 'minute'
|
||||||
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.
|
||||||
@@ -551,15 +547,6 @@ 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,
|
||||||
@@ -691,8 +678,6 @@ 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'
|
||||||
|
|
||||||
@@ -714,8 +699,6 @@ 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(
|
||||||
@@ -959,9 +942,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', '5-minute', 'minute'}
|
data_frequency : {'daily', 'minute'}
|
||||||
data_frequency tells the algorithm if it is running with
|
data_frequency tells the algorithm if it is running with
|
||||||
daily, minute, or five-minute mode.
|
daily or minute mode.
|
||||||
start : datetime
|
start : datetime
|
||||||
The start date for the simulation.
|
The start date for the simulation.
|
||||||
end : datetime
|
end : datetime
|
||||||
@@ -1136,17 +1119,10 @@ 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
|
|
||||||
|
|
||||||
date_rule = date_rule or date_rules.every_day()
|
date_rule = date_rule or date_rules.every_day()
|
||||||
if freq is 'daily':
|
time_rule = ((time_rule or time_rules.every_minute())
|
||||||
# ignore time rule in daily mode
|
if self.sim_params.data_frequency == 'minute' else
|
||||||
time_rule = time_rules.every_minute()
|
# If we are in daily mode the time_rule is ignored.
|
||||||
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())
|
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
|
||||||
@@ -1488,7 +1464,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)
|
amount = self.round_order(amount, asset)
|
||||||
|
|
||||||
# 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,
|
||||||
@@ -1505,16 +1481,13 @@ class TradingAlgorithm(object):
|
|||||||
return amount, style
|
return amount, style
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def round_order(amount):
|
def round_order(amount, asset):
|
||||||
"""
|
"""
|
||||||
Convert number of shares to an integer.
|
Converts the number of shares to the smallest tradable lot size for
|
||||||
|
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 int(round_if_near_integer(amount))
|
return round_nearest(amount, asset.min_trade_size)
|
||||||
|
|
||||||
def validate_order_params(self,
|
def validate_order_params(self,
|
||||||
asset,
|
asset,
|
||||||
@@ -1550,7 +1523,6 @@ 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):
|
||||||
"""
|
"""
|
||||||
@@ -1822,7 +1794,7 @@ class TradingAlgorithm(object):
|
|||||||
|
|
||||||
@data_frequency.setter
|
@data_frequency.setter
|
||||||
def data_frequency(self, value):
|
def data_frequency(self, value):
|
||||||
assert value in ('daily', '5-minute', 'minute')
|
assert value in ('daily', 'minute')
|
||||||
self.sim_params.data_frequency = value
|
self.sim_params.data_frequency = value
|
||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
|
|||||||
+71
-10
@@ -17,6 +17,8 @@
|
|||||||
"""
|
"""
|
||||||
Cythonized Asset object.
|
Cythonized Asset object.
|
||||||
"""
|
"""
|
||||||
|
import hashlib
|
||||||
|
|
||||||
cimport cython
|
cimport cython
|
||||||
from cpython.number cimport PyNumber_Index
|
from cpython.number cimport PyNumber_Index
|
||||||
from cpython.object cimport (
|
from cpython.object cimport (
|
||||||
@@ -36,6 +38,7 @@ 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
|
from catalyst.exchange.exchange_errors import InvalidSymbolError, SidHashError
|
||||||
|
|
||||||
@@ -59,6 +62,7 @@ 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',
|
||||||
@@ -70,6 +74,7 @@ cdef class Asset:
|
|||||||
'auto_close_date',
|
'auto_close_date',
|
||||||
'exchange',
|
'exchange',
|
||||||
'exchange_full',
|
'exchange_full',
|
||||||
|
'min_trade_size',
|
||||||
})
|
})
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
@@ -81,7 +86,8 @@ cdef class Asset:
|
|||||||
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)
|
||||||
@@ -94,6 +100,7 @@ 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
|
||||||
@@ -148,7 +155,8 @@ 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)
|
||||||
@@ -170,7 +178,8 @@ 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):
|
||||||
"""
|
"""
|
||||||
@@ -186,6 +195,7 @@ 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
|
||||||
@@ -234,7 +244,7 @@ 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')
|
'exchange_full', '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)
|
||||||
@@ -388,6 +398,9 @@ cdef class TradingPair(Asset):
|
|||||||
cdef readonly float leverage
|
cdef readonly float leverage
|
||||||
cdef readonly object market_currency
|
cdef readonly object market_currency
|
||||||
cdef readonly object base_currency
|
cdef readonly object base_currency
|
||||||
|
cdef readonly object end_daily
|
||||||
|
cdef readonly object end_minute
|
||||||
|
cdef readonly object exchange_symbol
|
||||||
|
|
||||||
_kwargnames = frozenset({
|
_kwargnames = frozenset({
|
||||||
'sid',
|
'sid',
|
||||||
@@ -401,7 +414,11 @@ cdef class TradingPair(Asset):
|
|||||||
'exchange_full',
|
'exchange_full',
|
||||||
'leverage',
|
'leverage',
|
||||||
'market_currency',
|
'market_currency',
|
||||||
'base_currency'
|
'base_currency',
|
||||||
|
'end_daily',
|
||||||
|
'end_minute',
|
||||||
|
'exchange_symbol',
|
||||||
|
'min_trade_size'
|
||||||
})
|
})
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
object symbol,
|
object symbol,
|
||||||
@@ -410,10 +427,14 @@ cdef class TradingPair(Asset):
|
|||||||
object asset_name=None,
|
object asset_name=None,
|
||||||
int sid=0,
|
int sid=0,
|
||||||
float leverage=1.0,
|
float leverage=1.0,
|
||||||
|
object end_daily=None,
|
||||||
|
object end_minute=None,
|
||||||
object end_date=None,
|
object end_date=None,
|
||||||
|
object exchange_symbol=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):
|
||||||
"""
|
"""
|
||||||
Replicates the Asset constructor with some built-in conventions
|
Replicates the Asset constructor with some built-in conventions
|
||||||
and a new 'leverage' attribute.
|
and a new 'leverage' attribute.
|
||||||
@@ -465,10 +486,14 @@ cdef class TradingPair(Asset):
|
|||||||
:param asset_name:
|
:param asset_name:
|
||||||
:param sid:
|
:param sid:
|
||||||
:param leverage:
|
:param leverage:
|
||||||
|
:param end_daily
|
||||||
|
:param end_minute
|
||||||
:param end_date:
|
:param end_date:
|
||||||
|
:param exchange_symbol:
|
||||||
:param first_traded:
|
:param first_traded:
|
||||||
:param auto_close_date:
|
:param auto_close_date:
|
||||||
:param exchange_full:
|
:param exchange_full:
|
||||||
|
:param min_trade_size:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
symbol = symbol.lower()
|
symbol = symbol.lower()
|
||||||
@@ -479,7 +504,7 @@ cdef class TradingPair(Asset):
|
|||||||
|
|
||||||
if sid == 0 or sid is None:
|
if sid == 0 or sid is None:
|
||||||
try:
|
try:
|
||||||
sid = abs(hash(symbol)) % (10 ** 4)
|
sid = get_sid(symbol)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise SidHashError(symbol=symbol)
|
raise SidHashError(symbol=symbol)
|
||||||
|
|
||||||
@@ -502,25 +527,60 @@ cdef class TradingPair(Asset):
|
|||||||
first_traded=first_traded,
|
first_traded=first_traded,
|
||||||
auto_close_date=auto_close_date,
|
auto_close_date=auto_close_date,
|
||||||
exchange_full=exchange_full,
|
exchange_full=exchange_full,
|
||||||
|
min_trade_size=min_trade_size
|
||||||
)
|
)
|
||||||
|
|
||||||
self.leverage = leverage
|
self.leverage = leverage
|
||||||
|
self.end_daily = end_daily
|
||||||
|
self.end_minute = end_minute
|
||||||
|
self.exchange_symbol = exchange_symbol
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \
|
return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \
|
||||||
'Introduced On: {start_date}, ' \
|
'Introduced On: {start_date}, ' \
|
||||||
'Market Currency: {market_currency}, ' \
|
'Market Currency: {market_currency}, ' \
|
||||||
'Base Currency: {base_currency}, ' \
|
'Base Currency: {base_currency}, ' \
|
||||||
'Exchange Leverage: {leverage}'.format(
|
'Exchange Leverage: {leverage}, ' \
|
||||||
|
'Minimum Trade Size: {min_trade_size} ' \
|
||||||
|
'Last daily ingestion: {end_daily} ' \
|
||||||
|
'Last minutely ingestion: {end_minute}'.format(
|
||||||
symbol=self.symbol,
|
symbol=self.symbol,
|
||||||
sid=self.sid,
|
sid=self.sid,
|
||||||
exchange=self.exchange,
|
exchange=self.exchange,
|
||||||
start_date=self.start_date,
|
start_date=self.start_date,
|
||||||
market_currency=self.market_currency,
|
market_currency=self.market_currency,
|
||||||
base_currency=self.base_currency,
|
base_currency=self.base_currency,
|
||||||
leverage=self.leverage
|
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: consider implementing to spot holds
|
||||||
|
return True
|
||||||
|
|
||||||
cpdef __reduce__(self):
|
cpdef __reduce__(self):
|
||||||
"""
|
"""
|
||||||
Function used by pickle to determine how to serialize/deserialize this
|
Function used by pickle to determine how to serialize/deserialize this
|
||||||
@@ -537,7 +597,8 @@ cdef class TradingPair(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))
|
||||||
|
|
||||||
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,7 +39,8 @@ 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,6 +73,7 @@ _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
|
||||||
@@ -390,6 +391,8 @@ 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,7 +76,9 @@ from catalyst.utils.numpy_utils import as_column
|
|||||||
from catalyst.utils.preprocess import preprocess
|
from catalyst.utils.preprocess import preprocess
|
||||||
from catalyst.utils.sqlite_utils import group_into_chunks, coerce_string_to_eng
|
from catalyst.utils.sqlite_utils import group_into_chunks, coerce_string_to_eng
|
||||||
|
|
||||||
log = Logger('assets.py')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('assets.py', level=LOG_LEVEL)
|
||||||
|
|
||||||
# A set of fields that need to be converted to strings before building an
|
# A set of fields that need to be converted to strings before building an
|
||||||
# Asset to avoid unicode fields
|
# Asset to avoid unicode fields
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logbook
|
||||||
|
|
||||||
|
''' You can override the LOG level from your environment.
|
||||||
|
For example, if you want to see the DEBUG messages, run:
|
||||||
|
$ export CATALYST_LOG_LEVEL=10
|
||||||
|
'''
|
||||||
|
LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO))
|
||||||
|
|
||||||
|
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
||||||
|
'{exchange}/symbols.json'
|
||||||
|
|
||||||
|
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M'
|
||||||
|
DATE_FORMAT = '%Y-%m-%d'
|
||||||
|
|
||||||
|
AUTO_INGEST = False
|
||||||
+298
-66
@@ -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
|
import os, time, shutil, requests, logbook
|
||||||
import time
|
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename
|
||||||
import requests
|
|
||||||
import logbook
|
|
||||||
|
|
||||||
DT_START = time.mktime(datetime(2010, 1, 1, 0, 0).timetuple())
|
|
||||||
CSV_OUT_FOLDER = '/var/tmp/catalyst/data/poloniex/'
|
DT_START = int(time.mktime(datetime(2010, 1, 1, 0, 0).timetuple()))
|
||||||
|
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,10 +26,15 @@ 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: %s' % CSV_OUT_FOLDER)
|
log.error('Failed to create data folder: {}'.format(
|
||||||
|
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:
|
||||||
@@ -45,100 +50,327 @@ 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: %d' % (len(self.currency_pairs)))
|
log.debug('Currency pairs retrieved successfully: {}'.format(
|
||||||
|
len(self.currency_pairs)
|
||||||
|
))
|
||||||
|
|
||||||
def _get_start_date(self, csv_fn):
|
|
||||||
''' Function returns latest appended date, if the file has been previously written
|
|
||||||
the last line is an empty one, so we have to read the second to last line
|
def _retrieve_tradeID_date(self, row):
|
||||||
|
'''
|
||||||
|
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) # First check file is not zero size
|
f.seek(0, os.SEEK_END)
|
||||||
if(f.tell() > 2):
|
if(f.tell() > 2): # Check file size is not 0
|
||||||
f.seek(-2, os.SEEK_END) # Jump to the second last byte.
|
f.seek(0) # Go to start to read
|
||||||
|
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.
|
||||||
lastrow = f.readline()
|
first_tradeID, start_file = self._retrieve_tradeID_date(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: %s' % csv_fn)
|
log.error('Error opening file: {}'.format(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
|
||||||
|
|
||||||
def get_data(self, currencyPair, start, end=9999999999, period=300):
|
log.debug('{}: Retrieving from {} to {}\t {} - {}'.format(
|
||||||
url = self._api_path + 'command=returnChartData¤cyPair=' + currencyPair + '&start=' + str(start) + '&end=' + str(end) + '&period=' + str(period)
|
currencyPair, str(newstart), str(end),
|
||||||
|
time.ctime(newstart), time.ctime(end)))
|
||||||
|
|
||||||
|
url = '{path}command=returnTradeHistory¤cyPair={pair}' \
|
||||||
|
'&start={start}&end={end}'.format(
|
||||||
|
path = self._api_path,
|
||||||
|
pair = currencyPair,
|
||||||
|
start = str(newstart),
|
||||||
|
end = str(end)
|
||||||
|
)
|
||||||
|
print url
|
||||||
|
|
||||||
|
attempts = 0
|
||||||
|
success = 0
|
||||||
|
while attempts < CONN_RETRIES:
|
||||||
try:
|
try:
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Failed to retrieve candlestick chart data for %s' % currencyPair)
|
log.error('Failed to retrieve trade history data for {}'.format(
|
||||||
|
currencyPair
|
||||||
|
))
|
||||||
log.exception(e)
|
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()
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Pulls latest data for a single pair
|
If we get to transactionId == 1, and we already have that on
|
||||||
|
disk, we got to the end of TradeHistory for this coin.
|
||||||
|
'''
|
||||||
|
if('first_tradeID' in locals()
|
||||||
|
and response.json()[-1]['tradeID'] == first_tradeID):
|
||||||
|
return
|
||||||
|
|
||||||
|
'''
|
||||||
|
There are primarily two scenarios:
|
||||||
|
a) There is newer data available that we need to add at
|
||||||
|
the beginning of the file. We'll retrieve all what we
|
||||||
|
need until we get to what we already have, writing it
|
||||||
|
to a temporary file; and we will write that at the
|
||||||
|
beginning of our existing file.
|
||||||
|
b) We are going back in time, appending at the end of
|
||||||
|
our existing TradeHistory until the first transaction
|
||||||
|
for this currencyPair
|
||||||
'''
|
'''
|
||||||
def append_data_single_pair(self, currencyPair, repeat=0):
|
|
||||||
log.debug('Getting data for %s' % currencyPair)
|
|
||||||
csv_fn = CSV_OUT_FOLDER + 'crypto_prices-' + currencyPair + '.csv'
|
|
||||||
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:
|
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:
|
with open(csv_fn, 'ab') as csvfile:
|
||||||
csvwriter = csv.writer(csvfile)
|
csvwriter = csv.writer(csvfile)
|
||||||
for item in data:
|
for item in response.json():
|
||||||
if item['date'] == 0:
|
if( 'first_tradeID' in locals()
|
||||||
|
and item['tradeID'] >= first_tradeID ):
|
||||||
continue
|
continue
|
||||||
csvwriter.writerow([
|
csvwriter.writerow([
|
||||||
|
item['tradeID'],
|
||||||
item['date'],
|
item['date'],
|
||||||
item['open'],
|
item['type'],
|
||||||
item['high'],
|
item['rate'],
|
||||||
item['low'],
|
item['amount'],
|
||||||
item['close'],
|
item['total'],
|
||||||
item['volume'],
|
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))
|
||||||
|
log.exception(e)
|
||||||
|
|
||||||
|
'''
|
||||||
|
If we got here, we aren't done yet. Call recursively with
|
||||||
|
'end' times that go sequentially back in time.
|
||||||
|
'''
|
||||||
|
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:
|
except Exception as e:
|
||||||
log.error('Error opening %s' % csv_fn)
|
log.error('Error opening {}'.format(csv_fn))
|
||||||
log.exception(e)
|
log.exception(e)
|
||||||
elif (repeat < CONN_RETRIES):
|
log.debug('{}: Generated 1min OHLCV data.'.format(currencyPair))
|
||||||
log.debug('Retrying: attemt %d' % (repeat+1) )
|
|
||||||
self.append_data_single_pair(currencyPair, repeat + 1)
|
|
||||||
|
|
||||||
'''
|
|
||||||
Pulls latest data for all currency pairs
|
|
||||||
'''
|
|
||||||
def append_data(self):
|
|
||||||
for currencyPair in self.currency_pairs:
|
|
||||||
self.append_data_single_pair(currencyPair)
|
|
||||||
# Rate limit is 6 calls per second, sleep 1sec/6 to be safe
|
|
||||||
time.sleep(0.17)
|
|
||||||
|
|
||||||
'''
|
|
||||||
Returns a data frame for all pairs, or for the requests currency pair.
|
|
||||||
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
|
def onemin_to_dataframe(self, currencyPair, start, end):
|
||||||
df = pd.read_csv(csv_fn, names=['date', 'open', 'high', 'low', 'close', 'volume'])
|
'''
|
||||||
|
Returns a data frame for a given currencyPair from data on disk
|
||||||
|
'''
|
||||||
|
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['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.append_data()
|
#pc.generate_symbols_json()
|
||||||
|
|
||||||
|
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']:
|
if column_name in ['open', 'high', 'low', 'close', 'volume']:
|
||||||
where_nan = (outbuf == 0)
|
where_nan = (outbuf == 0)
|
||||||
outbuf_as_float = outbuf.astype(float64) * .000001
|
outbuf_as_float = outbuf.astype(float64) * .000000001
|
||||||
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 != 'volume':
|
elif column_name in ['volume']:
|
||||||
results.append(outbuf.astype(uint32))
|
results.append(outbuf.astype(float64) * .000000001)
|
||||||
else:
|
else:
|
||||||
results.append(outbuf)
|
results.append(outbuf)
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -35,17 +35,6 @@ 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,
|
||||||
@@ -99,26 +88,6 @@ 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,
|
||||||
@@ -189,50 +158,3 @@ 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,8 +30,10 @@ from catalyst.utils.cli import (
|
|||||||
)
|
)
|
||||||
from catalyst.utils.memoize import lazyval
|
from catalyst.utils.memoize import lazyval
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
logbook.StderrHandler().push_application()
|
logbook.StderrHandler().push_application()
|
||||||
log = logbook.Logger(__name__)
|
log = logbook.Logger(__name__, level=LOG_LEVEL)
|
||||||
|
|
||||||
DEFAULT_RETRIES = 5
|
DEFAULT_RETRIES = 5
|
||||||
|
|
||||||
@@ -60,10 +62,6 @@ 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()
|
||||||
@@ -115,7 +113,6 @@ 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,
|
||||||
@@ -162,7 +159,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
|
||||||
# either minute or 5-minute data.
|
# minute data.
|
||||||
metadata = self._post_process_metadata(
|
metadata = self._post_process_metadata(
|
||||||
raw_metadata,
|
raw_metadata,
|
||||||
cache,
|
cache,
|
||||||
@@ -170,26 +167,6 @@ 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:
|
||||||
@@ -491,7 +468,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,6 +24,7 @@ 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
|
||||||
@@ -46,10 +47,6 @@ 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 []
|
||||||
@@ -67,10 +64,6 @@ 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,10 +17,6 @@ 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,
|
||||||
@@ -54,11 +50,6 @@ 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(
|
||||||
@@ -92,8 +83,6 @@ 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'
|
||||||
@@ -206,14 +195,13 @@ 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 five_minute_bar_reader daily_bar_reader '
|
'asset_finder minute_bar_reader daily_bar_reader '
|
||||||
'adjustment_reader',
|
'adjustment_reader',
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -303,7 +291,6 @@ 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,
|
||||||
@@ -316,7 +303,6 @@ 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.
|
||||||
|
|
||||||
@@ -397,7 +383,6 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -496,16 +481,6 @@ 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)
|
||||||
@@ -532,7 +507,6 @@ 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
|
||||||
@@ -544,7 +518,6 @@ 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,
|
||||||
@@ -631,9 +604,6 @@ 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,6 +13,8 @@
|
|||||||
# 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
|
||||||
@@ -23,6 +25,8 @@ 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):
|
||||||
@@ -36,7 +40,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
def frequencies(self):
|
def frequencies(self):
|
||||||
return set((
|
return set((
|
||||||
'daily',
|
'daily',
|
||||||
#'5-minute',
|
'minute',
|
||||||
))
|
))
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
@@ -75,12 +79,14 @@ 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,
|
||||||
@@ -90,6 +96,14 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
frequency):
|
frequency):
|
||||||
|
|
||||||
|
# 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(
|
raw = pd.read_json(
|
||||||
self._format_data_url(
|
self._format_data_url(
|
||||||
api_key,
|
api_key,
|
||||||
@@ -105,7 +119,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
# BcolzDailyBarReader introduces a 1/1000 factor in the way pricing is stored
|
# BcolzDailyBarReader introduces a 1/1000 factor in the way pricing is stored
|
||||||
# on disk, which we compensate here to get the right pricing amounts
|
# on disk, which we compensate here to get the right pricing amounts
|
||||||
# ref: data/us_equity_pricing.py
|
# ref: data/us_equity_pricing.py
|
||||||
scale = 1000
|
scale = 1
|
||||||
raw.loc[:, 'open'] /= scale
|
raw.loc[:, 'open'] /= scale
|
||||||
raw.loc[:, 'high'] /= scale
|
raw.loc[:, 'high'] /= scale
|
||||||
raw.loc[:, 'low'] /= scale
|
raw.loc[:, 'low'] /= scale
|
||||||
@@ -134,7 +148,6 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
data_frequency):
|
data_frequency):
|
||||||
period_map = {
|
period_map = {
|
||||||
'daily': 86400,
|
'daily': 86400,
|
||||||
# '5-minute': 300,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -153,6 +166,7 @@ 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),
|
||||||
)
|
)
|
||||||
@@ -166,4 +180,9 @@ register_bundle(PoloniexBundle, ['USDT_BTC',])
|
|||||||
For a production environment make sure to use (to bundle all pairs):
|
For a production environment make sure to use (to bundle all pairs):
|
||||||
register_bundle(PoloniexBundle)
|
register_bundle(PoloniexBundle)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
if 'ingest' in sys.argv and '-c' in sys.argv:
|
||||||
|
register_bundle(PoloniexBundle)
|
||||||
|
else:
|
||||||
register_bundle(PoloniexBundle, create_writers=False)
|
register_bundle(PoloniexBundle, create_writers=False)
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ from catalyst.utils.cli import maybe_show_progress
|
|||||||
|
|
||||||
from . import core as bundles
|
from . import core as bundles
|
||||||
|
|
||||||
log = Logger(__name__)
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
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,7 +42,6 @@ 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 (
|
||||||
@@ -69,7 +68,9 @@ from catalyst.errors import (
|
|||||||
HistoryWindowStartsBeforeData,
|
HistoryWindowStartsBeforeData,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = Logger('DataPortal')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('DataPortal', level=LOG_LEVEL)
|
||||||
|
|
||||||
BASE_FIELDS = frozenset([
|
BASE_FIELDS = frozenset([
|
||||||
"open",
|
"open",
|
||||||
@@ -120,10 +121,6 @@ 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
|
||||||
@@ -150,7 +147,6 @@ 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,
|
||||||
@@ -202,7 +198,6 @@ 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
|
||||||
@@ -214,8 +209,6 @@ 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(
|
||||||
@@ -229,13 +222,10 @@ 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
|
||||||
|
|
||||||
@@ -267,13 +257,6 @@ 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,
|
||||||
@@ -283,7 +266,6 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,17 +701,6 @@ 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,6 +18,7 @@ from numpy import (
|
|||||||
full,
|
full,
|
||||||
nan,
|
nan,
|
||||||
int64,
|
int64,
|
||||||
|
float64,
|
||||||
zeros
|
zeros
|
||||||
)
|
)
|
||||||
from six import iteritems, with_metaclass
|
from six import iteritems, with_metaclass
|
||||||
@@ -70,7 +71,9 @@ 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' and field != 'sid':
|
if field == 'volume':
|
||||||
|
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)
|
||||||
@@ -135,12 +138,6 @@ 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 = 3
|
DEFAULT_ASSET_PRICE_DECIMALS = 9
|
||||||
|
|
||||||
|
|
||||||
class HistoryCompatibleUSEquityAdjustmentReader(object):
|
class HistoryCompatibleUSEquityAdjustmentReader(object):
|
||||||
|
|||||||
+81
-52
@@ -12,32 +12,29 @@
|
|||||||
# 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 numpy as np
|
|
||||||
from pandas_datareader.data import DataReader
|
|
||||||
import datetime
|
|
||||||
import time
|
|
||||||
import pytz
|
import pytz
|
||||||
|
from pandas_datareader.data import DataReader
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
from six.moves.urllib_error import HTTPError
|
from six.moves.urllib_error import HTTPError
|
||||||
|
|
||||||
from .benchmarks import get_benchmark_returns
|
from catalyst.utils.calendars import get_calendar
|
||||||
from . import treasuries, treasuries_can
|
from . import treasuries, treasuries_can
|
||||||
|
from .benchmarks import get_benchmark_returns
|
||||||
|
from ..utils.deprecate import deprecated
|
||||||
from ..utils.paths import (
|
from ..utils.paths import (
|
||||||
cache_root,
|
cache_root,
|
||||||
data_root,
|
data_root,
|
||||||
)
|
)
|
||||||
from ..utils.deprecate import deprecated
|
|
||||||
|
|
||||||
from catalyst.data.bundles.poloniex import PoloniexBundle
|
from catalyst.constants import LOG_LEVEL
|
||||||
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 = {
|
||||||
@@ -94,18 +91,27 @@ 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 (last >= last_date.tz_localize(None))
|
return (first <= first_date.tz_localize(None)) and (
|
||||||
|
last >= last_date.tz_localize(None))
|
||||||
|
|
||||||
def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT_BTC',
|
|
||||||
bundle=None, bundle_data=None, environ=None):
|
|
||||||
|
|
||||||
|
def load_crypto_market_data(trading_day=None, trading_days=None,
|
||||||
|
bm_symbol=None, bundle=None, bundle_data=None,
|
||||||
|
environ=None, exchange=None, start_dt=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
|
|
||||||
|
|
||||||
first_date = trading_days[1]
|
# TODO: consider making configurable
|
||||||
now = pd.Timestamp.utcnow()
|
bm_symbol = 'btc_usdt'
|
||||||
|
# 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
|
||||||
@@ -121,6 +127,7 @@ def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT
|
|||||||
|
|
||||||
# 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.
|
||||||
|
'''
|
||||||
if(bundle_data):
|
if(bundle_data):
|
||||||
# If we are using the bundle to retrieve the cryptobenchmark, find the last
|
# If we are using the bundle to retrieve the cryptobenchmark, find the last
|
||||||
# date for which there is trading data in the bundle
|
# date for which there is trading data in the bundle
|
||||||
@@ -129,19 +136,32 @@ def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT
|
|||||||
last_date = pd.to_datetime(bundle_data.daily_bar_reader._spot_col('day')[ix],unit='s')
|
last_date = pd.to_datetime(bundle_data.daily_bar_reader._spot_col('day')[ix],unit='s')
|
||||||
else:
|
else:
|
||||||
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
||||||
|
'''
|
||||||
|
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.poloniex.poloniex import Poloniex
|
||||||
|
exchange = Poloniex('', '', '')
|
||||||
|
|
||||||
|
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,
|
|
||||||
bundle,
|
|
||||||
bundle_data,
|
|
||||||
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-02', tz='UTC')
|
||||||
@@ -149,11 +169,12 @@ def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT
|
|||||||
bm_symbol,
|
bm_symbol,
|
||||||
first_date_treasury,
|
first_date_treasury,
|
||||||
last_date,
|
last_date,
|
||||||
now,
|
end_dt,
|
||||||
environ,
|
environ,
|
||||||
)
|
)
|
||||||
benchmark_returns = br[br.index.slice_indexer(first_date, last_date)]
|
benchmark_returns = br[br.index.slice_indexer(start_dt, last_date)]
|
||||||
treasury_curves = tc[tc.index.slice_indexer(first_date_treasury, last_date)]
|
treasury_curves = tc[
|
||||||
|
tc.index.slice_indexer(first_date_treasury, last_date)]
|
||||||
return benchmark_returns, treasury_curves
|
return benchmark_returns, treasury_curves
|
||||||
|
|
||||||
|
|
||||||
@@ -251,7 +272,6 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
bundle,
|
bundle,
|
||||||
bundle_data,
|
bundle_data,
|
||||||
environ=None):
|
environ=None):
|
||||||
|
|
||||||
filename = get_benchmark_filename(symbol)
|
filename = get_benchmark_filename(symbol)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -285,43 +305,51 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
prevents users abroad from getting Catalyst to work
|
prevents users abroad from getting Catalyst to work
|
||||||
'''
|
'''
|
||||||
logger.info(
|
logger.info(
|
||||||
('Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'),
|
(
|
||||||
|
'Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'),
|
||||||
symbol=symbol, first_date=first_date, last_date=last_date)
|
symbol=symbol, first_date=first_date, last_date=last_date)
|
||||||
|
|
||||||
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,as_of_date=None)
|
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,
|
||||||
|
as_of_date=None)
|
||||||
fields = ['day', 'close']
|
fields = ['day', 'close']
|
||||||
raw = bundle_data.daily_bar_reader.load_raw_arrays(
|
raw = bundle_data.daily_bar_reader.load_raw_arrays(
|
||||||
columns=fields,
|
columns=fields,
|
||||||
start_date=first_date - trading_day,
|
start_date=first_date - trading_day,
|
||||||
end_date=last_date,
|
end_date=last_date,
|
||||||
assets=[asset, ])
|
assets=[asset, ])
|
||||||
bench_raw = pd.concat([pd.DataFrame(raw[0], columns=['date']),pd.DataFrame(raw[1], columns=['close'])], axis=1)
|
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['date'] = pd.to_datetime(bench_raw['date'], unit='s')
|
||||||
bench_raw.set_index('date', inplace=True)
|
bench_raw.set_index('date', inplace=True)
|
||||||
bench_raw.sort_index(inplace=True)
|
bench_raw.sort_index(inplace=True)
|
||||||
bench_raw = bench_raw[pd.to_datetime(first_date - trading_day):pd.to_datetime(last_date)]
|
bench_raw = bench_raw[
|
||||||
|
pd.to_datetime(first_date - trading_day):pd.to_datetime(
|
||||||
|
last_date)]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# This is how it used to be: downloading the benchmark everytime.
|
# This is how it used to be: downloading the benchmark everytime.
|
||||||
# Leaving this code here to be repurposed in the future for other bundles.
|
# Leaving this code here to be repurposed in the future for other bundles.
|
||||||
logger.info(
|
logger.info(
|
||||||
('Downloading benchmark data for {symbol!r} from {first_date} to {last_date}'),
|
(
|
||||||
|
'Downloading benchmark data for {symbol!r} from {first_date} to {last_date}'),
|
||||||
symbol=symbol, first_date=first_date, last_date=last_date)
|
symbol=symbol, first_date=first_date, last_date=last_date)
|
||||||
|
|
||||||
|
raise DeprecationWarning('poloniex bundle deprecated')
|
||||||
# Load benchmark symbol from Poloniex API
|
# Load benchmark symbol from Poloniex API
|
||||||
try:
|
# try:
|
||||||
bundle = PoloniexBundle()
|
# bundle = PoloniexBundle()
|
||||||
bench_raw = bundle._fetch_symbol_frame(
|
# bench_raw = bundle._fetch_symbol_frame(
|
||||||
None,
|
# None,
|
||||||
symbol,
|
# symbol,
|
||||||
get_calendar(bundle.calendar_name),
|
# get_calendar(bundle.calendar_name),
|
||||||
first_date - trading_day,
|
# first_date - trading_day,
|
||||||
last_date,
|
# last_date,
|
||||||
'daily',
|
# 'daily',
|
||||||
)
|
# )
|
||||||
except (OSError, IOError, HTTPError):
|
# except (OSError, IOError, HTTPError):
|
||||||
logger.exception('Failed to fetch new crypto benchmark returns')
|
# logger.exception('Failed to fetch new crypto benchmark returns')
|
||||||
raise
|
# 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']]
|
||||||
@@ -525,7 +553,8 @@ 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, errors='coerce' ).tz_localize('UTC')
|
data.index = pd.to_datetime(data.index, infer_datetime_format=True,
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -39,20 +39,21 @@ 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_uint32_safe
|
from catalyst.data.us_equity_pricing import check_uint64_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')
|
logger = logbook.Logger('MinuteBars', level=LOG_LEVEL)
|
||||||
|
|
||||||
US_EQUITIES_MINUTES_PER_DAY = 390
|
US_EQUITIES_MINUTES_PER_DAY = 390
|
||||||
FUTURES_MINUTES_PER_DAY = 1440
|
FUTURES_MINUTES_PER_DAY = 1440
|
||||||
|
|
||||||
DEFAULT_EXPECTEDLEN = US_EQUITIES_MINUTES_PER_DAY * 252 * 15
|
DEFAULT_EXPECTEDLEN = US_EQUITIES_MINUTES_PER_DAY * 252 * 15
|
||||||
|
|
||||||
OHLC_RATIO = 1000
|
OHLC_RATIO = 100000000
|
||||||
|
|
||||||
|
|
||||||
class BcolzMinuteOverlappingData(Exception):
|
class BcolzMinuteOverlappingData(Exception):
|
||||||
@@ -114,15 +115,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 uint32 columns.
|
"""Adapt OHLCV columns into uint64 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 uint32.
|
to a float column to convert to uint64.
|
||||||
scale_factor : int
|
scale_factor : int
|
||||||
Factor to use to scale float values before converting to uint32.
|
Factor to use to scale float values before converting to uint64.
|
||||||
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
|
||||||
@@ -135,6 +136,7 @@ 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)
|
||||||
|
|
||||||
@@ -143,11 +145,12 @@ 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_uint32_safe(max_val, col_name)
|
check_uint64_safe(max_val, col_name)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
if invalid_data_behavior == 'raise':
|
if invalid_data_behavior == 'raise':
|
||||||
raise
|
raise
|
||||||
@@ -155,20 +158,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 '
|
||||||
'uint32 (max={}), filtering them out',
|
'uint64 (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.uint32).max)
|
exclude_mask &= (scaled_col >= np.iinfo(np.uint64).max)
|
||||||
|
|
||||||
# Convert all cols to uint32.
|
# Convert all cols to uint32.
|
||||||
opens = scaled_opens.astype(np.uint32)
|
opens = scaled_opens.astype(np.uint64)
|
||||||
highs = scaled_highs.astype(np.uint32)
|
highs = scaled_highs.astype(np.uint64)
|
||||||
lows = scaled_lows.astype(np.uint32)
|
lows = scaled_lows.astype(np.uint64)
|
||||||
closes = scaled_closes.astype(np.uint32)
|
closes = scaled_closes.astype(np.uint64)
|
||||||
volumes = cols['volume'].astype(np.uint32)
|
volumes = scaled_volumes.astype(np.uint64)
|
||||||
|
|
||||||
# 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
|
||||||
@@ -288,7 +291,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.uint32. If ohlc_ratios_per_sid is None or does not
|
the np.uint64. 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
|
||||||
@@ -372,13 +375,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.uint32. If
|
convert from floats to integers that fit within np.uint64. 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 (1000).
|
given sid, this ratio is used. Default is OHLC_RATIO (10^8).
|
||||||
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.uint32.
|
an integer to fit within the np.uint64.
|
||||||
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.
|
||||||
@@ -401,11 +404,9 @@ 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, and close columns are integers which are 1000 times
|
The open, high, low, close and volume columns are integers which are 10^8 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.uint32, supporting market prices quoted up to the thousands place.
|
np.uint64, supporting market prices quoted up to the 1/10^8-th 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.
|
||||||
@@ -573,7 +574,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.uint32)
|
initial_array = np.empty(0, np.uint64)
|
||||||
table = ctable(
|
table = ctable(
|
||||||
rootdir=path,
|
rootdir=path,
|
||||||
columns=[
|
columns=[
|
||||||
@@ -610,7 +611,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.uint32)
|
prepend_array = np.zeros(num_to_prepend, np.uint64)
|
||||||
# Fill all OHLCV with zeros.
|
# Fill all OHLCV with zeros.
|
||||||
table.append([prepend_array] * 5)
|
table.append([prepend_array] * 5)
|
||||||
table.flush()
|
table.flush()
|
||||||
@@ -815,11 +816,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.uint32)
|
open_col = np.zeros(minutes_count, dtype=np.uint64)
|
||||||
high_col = np.zeros(minutes_count, dtype=np.uint32)
|
high_col = np.zeros(minutes_count, dtype=np.uint64)
|
||||||
low_col = np.zeros(minutes_count, dtype=np.uint32)
|
low_col = np.zeros(minutes_count, dtype=np.uint64)
|
||||||
close_col = np.zeros(minutes_count, dtype=np.uint32)
|
close_col = np.zeros(minutes_count, dtype=np.uint64)
|
||||||
vol_col = np.zeros(minutes_count, dtype=np.uint32)
|
vol_col = np.zeros(minutes_count, dtype=np.uint64)
|
||||||
|
|
||||||
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]'))
|
||||||
@@ -1125,7 +1126,7 @@ 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
|
||||||
|
|
||||||
@@ -1248,7 +1249,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.uint32)
|
out = np.zeros(shape, dtype=np.float64)
|
||||||
|
|
||||||
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)
|
||||||
@@ -1262,11 +1263,11 @@ class BcolzMinuteBarReader(MinuteBarReader):
|
|||||||
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
|
||||||
@@ -1353,6 +1354,7 @@ 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,7 +156,10 @@ 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,6 +11,9 @@
|
|||||||
# 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
|
||||||
@@ -80,8 +83,9 @@ from catalyst.utils.cli import (
|
|||||||
from ._equities import _compute_row_slices, _read_bcolz_data
|
from ._equities import _compute_row_slices, _read_bcolz_data
|
||||||
from ._adjustments import load_adjustments_from_sqlite
|
from ._adjustments import load_adjustments_from_sqlite
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
logger = logbook.Logger('UsEquityPricing')
|
logger = logbook.Logger('UsEquityPricing', level=LOG_LEVEL)
|
||||||
|
|
||||||
OHLC = frozenset(['open', 'high', 'low', 'close'])
|
OHLC = frozenset(['open', 'high', 'low', 'close'])
|
||||||
OHLCV = frozenset(['open', 'high', 'low', 'close', 'volume'])
|
OHLCV = frozenset(['open', 'high', 'low', 'close', 'volume'])
|
||||||
@@ -116,6 +120,8 @@ SQLITE_STOCK_DIVIDEND_PAYOUT_COLUMN_DTYPES = {
|
|||||||
UINT32_MAX = iinfo(uint32).max
|
UINT32_MAX = iinfo(uint32).max
|
||||||
UINT64_MAX = iinfo(uint64).max
|
UINT64_MAX = iinfo(uint64).max
|
||||||
|
|
||||||
|
PRICE_ADJUSTMENT_FACTOR = 1000000000 # Provides 9 decimals resolution. Also affects _equities.pyx L220
|
||||||
|
|
||||||
|
|
||||||
def check_uint32_safe(value, colname):
|
def check_uint32_safe(value, colname):
|
||||||
if value >= UINT32_MAX:
|
if value >= UINT32_MAX:
|
||||||
@@ -433,11 +439,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)] * 1000000).astype('uint64')
|
processed = (raw_data[list(OHLC)] * PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
||||||
dates = raw_data.index.values.astype('datetime64[s]')
|
dates = raw_data.index.values.astype('datetime64[s]')
|
||||||
check_uint32_safe(dates.max().view(np.int64), 'day')
|
check_uint32_safe(dates.max().view(np.int64), 'day')
|
||||||
processed['day'] = dates.astype('uint32')
|
processed['day'] = dates.astype('uint32')
|
||||||
processed['volume'] = raw_data.volume.astype('uint64')
|
processed['volume'] = (raw_data.volume * PRICE_ADJUSTMENT_FACTOR).astype('uint64')
|
||||||
return ctable.fromdataframe(processed)
|
return ctable.fromdataframe(processed)
|
||||||
|
|
||||||
|
|
||||||
@@ -490,9 +496,8 @@ 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') are interpreted as 1000 *
|
- Price columns ('open', 'high', 'low', 'close') and Volume are interpreted
|
||||||
as-traded dollar value.
|
as 10^9 * 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.
|
||||||
|
|
||||||
@@ -519,7 +524,6 @@ 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
|
||||||
@@ -759,13 +763,10 @@ 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':
|
if field != 'volume' and price == 0:
|
||||||
if price == 0:
|
|
||||||
return nan
|
return nan
|
||||||
else:
|
else:
|
||||||
return price * 0.001
|
return price / PRICE_ADJUSTMENT_FACTOR
|
||||||
else:
|
|
||||||
return price
|
|
||||||
|
|
||||||
|
|
||||||
class PanelBarReader(SessionBarReader):
|
class PanelBarReader(SessionBarReader):
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
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')
|
||||||
|
|
||||||
|
base_currency = enter_exchange.base_currency
|
||||||
|
base_currency_amount = enter_exchange.portfolio.cash
|
||||||
|
|
||||||
|
exit_balances = exit_exchange.get_balances()
|
||||||
|
exit_currency = context.trading_pairs[
|
||||||
|
context.selling_exchange].market_currency
|
||||||
|
|
||||||
|
if exit_currency in exit_balances:
|
||||||
|
market_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 base_currency_amount < (amount * entry_price):
|
||||||
|
adj_amount = base_currency_amount / entry_price
|
||||||
|
log.warn(
|
||||||
|
'not enough {base_currency} ({base_currency_amount}) to buy '
|
||||||
|
'{amount}, adjusting the amount to {adj_amount}'.format(
|
||||||
|
base_currency=base_currency,
|
||||||
|
base_currency_amount=base_currency_amount,
|
||||||
|
amount=amount,
|
||||||
|
adj_amount=adj_amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
amount = adj_amount
|
||||||
|
|
||||||
|
elif market_currency_amount < amount:
|
||||||
|
log.warn(
|
||||||
|
'not enough {currency} ({currency_amount}) to sell '
|
||||||
|
'{amount}, aborting'.format(
|
||||||
|
currency=exit_currency,
|
||||||
|
currency_amount=market_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,
|
||||||
|
base_currency='btc',
|
||||||
|
live_graph=False
|
||||||
|
)
|
||||||
@@ -14,7 +14,9 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import (
|
from catalyst.api import (
|
||||||
order_target_value,
|
order_target_value,
|
||||||
symbol,
|
symbol,
|
||||||
@@ -23,21 +25,18 @@ from catalyst.api import (
|
|||||||
get_open_orders,
|
get_open_orders,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.ASSET_NAME = 'USDT_BTC'
|
context.ASSET_NAME = 'btc_usd'
|
||||||
context.TARGET_HODL_RATIO = 0.8
|
context.TARGET_HODL_RATIO = 0.8
|
||||||
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
||||||
|
|
||||||
# For all trading pairs in the poloniex bundle, the default denomination
|
|
||||||
# currently supported by Catalyst is 1/1000th of a full coin. Use this
|
|
||||||
# constant to scale the price of up to that of a full coin if desired.
|
|
||||||
context.TICK_SIZE = 1000.0
|
|
||||||
|
|
||||||
context.is_buying = True
|
context.is_buying = True
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
context.i = 0
|
context.i = 0
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
context.i += 1
|
context.i += 1
|
||||||
|
|
||||||
@@ -56,10 +55,11 @@ def handle_data(context, data):
|
|||||||
context.is_buying = False
|
context.is_buying = False
|
||||||
|
|
||||||
# Retrieve current asset price from pricing data
|
# Retrieve current asset price from pricing data
|
||||||
price = data[context.asset].price
|
price = data.current(context.asset, 'price')
|
||||||
|
|
||||||
# Check if still buying and could (approximately) afford another purchase
|
# Check if still buying and could (approximately) afford another purchase
|
||||||
if context.is_buying and cash > price:
|
if context.is_buying and cash > price:
|
||||||
|
print('buying')
|
||||||
# Place order to make position in asset equal to target_hodl_value
|
# Place order to make position in asset equal to target_hodl_value
|
||||||
order_target_value(
|
order_target_value(
|
||||||
context.asset,
|
context.asset,
|
||||||
@@ -70,12 +70,13 @@ def handle_data(context, data):
|
|||||||
|
|
||||||
record(
|
record(
|
||||||
price=price,
|
price=price,
|
||||||
volume=data[context.asset].volume,
|
volume=data.current(context.asset, 'volume'),
|
||||||
cash=cash,
|
cash=cash,
|
||||||
starting_cash=context.portfolio.starting_cash,
|
starting_cash=context.portfolio.starting_cash,
|
||||||
leverage=context.account.leverage,
|
leverage=context.account.leverage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def analyze(context=None, results=None):
|
def analyze(context=None, results=None):
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
@@ -86,18 +87,19 @@ def analyze(context=None, results=None):
|
|||||||
|
|
||||||
ax2 = plt.subplot(612, sharex=ax1)
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
|
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
|
||||||
(context.TICK_SIZE * results[['price']]).plot(ax=ax2)
|
results[['price']].plot(ax=ax2)
|
||||||
|
|
||||||
trans = results.ix[[t != [] for t in results.transactions]]
|
trans = results.ix[[t != [] for t in results.transactions]]
|
||||||
buys = trans.ix[
|
buys = trans.ix[
|
||||||
[t[0]['amount'] > 0 for t in trans.transactions]
|
[t[0]['amount'] > 0 for t in trans.transactions]
|
||||||
]
|
]
|
||||||
ax2.plot(
|
ax2.scatter(
|
||||||
buys.index,
|
buys.index.to_pydatetime(),
|
||||||
context.TICK_SIZE * results.price[buys.index],
|
results.price[buys.index],
|
||||||
'^',
|
marker='^',
|
||||||
markersize=10,
|
s=100,
|
||||||
color='g',
|
c='g',
|
||||||
|
label=''
|
||||||
)
|
)
|
||||||
|
|
||||||
ax3 = plt.subplot(613, sharex=ax1)
|
ax3 = plt.subplot(613, sharex=ax1)
|
||||||
@@ -135,3 +137,18 @@ def analyze(context=None, results=None):
|
|||||||
# 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='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bitfinex',
|
||||||
|
algo_namespace='buy_and_hodl',
|
||||||
|
base_currency='usd',
|
||||||
|
start=pd.to_datetime('2017-11-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-11-10', utc=True),
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'''
|
||||||
|
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 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 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'))
|
||||||
@@ -27,7 +27,7 @@ log = Logger(algo_namespace)
|
|||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
log.info('initializing algo')
|
log.info('initializing algo')
|
||||||
context.ASSET_NAME = 'XRP_USD'
|
context.ASSET_NAME = 'XRP_USDT'
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
context.TARGET_POSITIONS = 5000
|
context.TARGET_POSITIONS = 5000
|
||||||
@@ -38,6 +38,8 @@ def initialize(context):
|
|||||||
context.retry_update_portfolio = 10
|
context.retry_update_portfolio = 10
|
||||||
context.retry_order = 5
|
context.retry_order = 5
|
||||||
|
|
||||||
|
context.swallow_errors = True
|
||||||
|
|
||||||
context.errors = []
|
context.errors = []
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ def _handle_data(context, data):
|
|||||||
bar_count=20,
|
bar_count=20,
|
||||||
frequency='15m'
|
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))
|
||||||
|
|
||||||
@@ -135,11 +138,11 @@ def _handle_data(context, data):
|
|||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
log.info('handling bar {}'.format(data.current_dt))
|
log.info('handling bar {}'.format(data.current_dt))
|
||||||
# try:
|
try:
|
||||||
_handle_data(context, data)
|
_handle_data(context, data)
|
||||||
# except Exception as e:
|
except Exception as e:
|
||||||
# log.warn('aborting the bar on error {}'.format(e))
|
log.warn('aborting the bar on error {}'.format(e))
|
||||||
# context.errors.append(e)
|
context.errors.append(e)
|
||||||
|
|
||||||
log.info('completed bar {}, total execution errors {}'.format(
|
log.info('completed bar {}, total execution errors {}'.format(
|
||||||
data.current_dt,
|
data.current_dt,
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.api import (
|
||||||
|
order,
|
||||||
|
order_target_percent,
|
||||||
|
symbol,
|
||||||
|
record,
|
||||||
|
get_open_orders,
|
||||||
|
)
|
||||||
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
|
algo_namespace = 'buy_the_dip_live'
|
||||||
|
log = Logger('buy low sell high')
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
log.info('initializing algo')
|
||||||
|
context.ASSET_NAME = 'btc_usdt'
|
||||||
|
context.asset = symbol(context.ASSET_NAME)
|
||||||
|
|
||||||
|
context.TARGET_POSITIONS = 30
|
||||||
|
context.PROFIT_TARGET = 0.1
|
||||||
|
context.SLIPPAGE_ALLOWED = 0.02
|
||||||
|
|
||||||
|
context.retry_check_open_orders = 10
|
||||||
|
context.retry_update_portfolio = 10
|
||||||
|
context.retry_order = 5
|
||||||
|
|
||||||
|
context.errors = []
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_data(context, data):
|
||||||
|
price = data.current(context.asset, 'price')
|
||||||
|
log.info('got price {price}'.format(price=price))
|
||||||
|
|
||||||
|
prices = data.history(
|
||||||
|
context.asset,
|
||||||
|
fields='price',
|
||||||
|
bar_count=20,
|
||||||
|
frequency='1d'
|
||||||
|
)
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
||||||
|
log.info('got rsi: {}'.format(rsi))
|
||||||
|
|
||||||
|
# Buying more when RSI is low, this should lower our cost basis
|
||||||
|
if rsi <= 30:
|
||||||
|
buy_increment = 1
|
||||||
|
elif rsi <= 40:
|
||||||
|
buy_increment = 0.5
|
||||||
|
elif rsi <= 70:
|
||||||
|
buy_increment = 0.2
|
||||||
|
else:
|
||||||
|
buy_increment = 0.1
|
||||||
|
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
log.info('base currency available: {cash}'.format(cash=cash))
|
||||||
|
|
||||||
|
record(
|
||||||
|
price=price,
|
||||||
|
rsi=rsi,
|
||||||
|
)
|
||||||
|
|
||||||
|
orders = get_open_orders(context.asset)
|
||||||
|
if orders:
|
||||||
|
log.info('skipping bar until all open orders execute')
|
||||||
|
return
|
||||||
|
|
||||||
|
is_buy = False
|
||||||
|
cost_basis = None
|
||||||
|
if context.asset in context.portfolio.positions:
|
||||||
|
position = context.portfolio.positions[context.asset]
|
||||||
|
|
||||||
|
cost_basis = position.cost_basis
|
||||||
|
log.info(
|
||||||
|
'found {amount} positions with cost basis {cost_basis}'.format(
|
||||||
|
amount=position.amount,
|
||||||
|
cost_basis=cost_basis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if position.amount >= context.TARGET_POSITIONS:
|
||||||
|
log.info('reached positions target: {}'.format(position.amount))
|
||||||
|
return
|
||||||
|
|
||||||
|
if price < cost_basis:
|
||||||
|
is_buy = True
|
||||||
|
elif position.amount > 0 and \
|
||||||
|
price > cost_basis * (1 + context.PROFIT_TARGET):
|
||||||
|
profit = (price * position.amount) - (cost_basis * position.amount)
|
||||||
|
log.info('closing position, taking profit: {}'.format(profit))
|
||||||
|
order_target_percent(
|
||||||
|
asset=context.asset,
|
||||||
|
target=0,
|
||||||
|
limit_price=price * (1 - context.SLIPPAGE_ALLOWED),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.info('no buy or sell opportunity found')
|
||||||
|
else:
|
||||||
|
is_buy = True
|
||||||
|
|
||||||
|
if is_buy:
|
||||||
|
if buy_increment is None:
|
||||||
|
log.info('the rsi is too high to consider buying {}'.format(rsi))
|
||||||
|
return
|
||||||
|
|
||||||
|
if price * buy_increment > cash:
|
||||||
|
log.info('not enough base currency to consider buying')
|
||||||
|
return
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
'buying position cheaper than cost basis {} < {}'.format(
|
||||||
|
price,
|
||||||
|
cost_basis
|
||||||
|
)
|
||||||
|
)
|
||||||
|
order(
|
||||||
|
asset=context.asset,
|
||||||
|
amount=buy_increment,
|
||||||
|
limit_price=price * (1 + context.SLIPPAGE_ALLOWED)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
log.info('handling bar {}'.format(data.current_dt))
|
||||||
|
# try:
|
||||||
|
_handle_data(context, data)
|
||||||
|
# except Exception as e:
|
||||||
|
# log.warn('aborting the bar on error {}'.format(e))
|
||||||
|
# context.errors.append(e)
|
||||||
|
|
||||||
|
log.info('completed bar {}, total execution errors {}'.format(
|
||||||
|
data.current_dt,
|
||||||
|
len(context.errors)
|
||||||
|
))
|
||||||
|
|
||||||
|
if len(context.errors) > 0:
|
||||||
|
log.info('the errors:\n{}'.format(context.errors))
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, stats):
|
||||||
|
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=100000,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
start=pd.to_datetime('2017-5-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-16', utc=True),
|
||||||
|
base_currency='usdt',
|
||||||
|
data_frequency='daily'
|
||||||
|
)
|
||||||
|
# run_algorithm(
|
||||||
|
# initialize=initialize,
|
||||||
|
# handle_data=handle_data,
|
||||||
|
# analyze=analyze,
|
||||||
|
# exchange_name='poloniex',
|
||||||
|
# live=True,
|
||||||
|
# algo_namespace=algo_namespace,
|
||||||
|
# base_currency='btc'
|
||||||
|
# )
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
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),
|
||||||
|
)
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
# For this example, we're going to write a simple momentum script. When the
|
||||||
|
# stock goes up quickly, we're going to buy; when it goes down quickly, we're
|
||||||
|
# going to sell. Hopefully we'll ride the waves.
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol, record, order_target_percent, get_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 Ether in USD Tether.
|
||||||
|
context.neo_eth = symbol('neo_eth')
|
||||||
|
context.base_price = None
|
||||||
|
context.current_day = None
|
||||||
|
|
||||||
|
context.RSI_OVERSOLD = 50
|
||||||
|
context.RSI_OVERBOUGHT = 80
|
||||||
|
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 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.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 ({})'.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')
|
||||||
|
|
||||||
|
ax6 = plt.subplot(615, sharex=ax1)
|
||||||
|
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
|
||||||
|
ax6.axhline(70, color='darkgoldenrod')
|
||||||
|
ax6.axhline(30, 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)
|
||||||
|
|
||||||
|
# Show the plot.
|
||||||
|
plt.gcf().set_size_inches(18, 8)
|
||||||
|
plt.show()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# The execution mode: backtest or live
|
||||||
|
MODE = 'backtest'
|
||||||
|
|
||||||
|
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 poloniex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=10000,
|
||||||
|
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-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.5,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bittrex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='eth',
|
||||||
|
live_graph=False
|
||||||
|
)
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
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),
|
||||||
|
)
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
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_usd')
|
||||||
|
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))
|
||||||
|
|
||||||
|
try:
|
||||||
|
prices = data.history(
|
||||||
|
context.asset,
|
||||||
|
fields='price',
|
||||||
|
bar_count=14,
|
||||||
|
frequency='15T'
|
||||||
|
)
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)[-1]
|
||||||
|
print('got rsi: {}'.format(rsi))
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
# 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='poloniex',
|
||||||
|
# live=True,
|
||||||
|
# algo_namespace='simple_loop',
|
||||||
|
# base_currency='eth',
|
||||||
|
# live_graph=False
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
# 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),
|
||||||
|
)
|
||||||
@@ -1,452 +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 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
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
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.exchange.stats_utils import get_pretty_stats
|
|
||||||
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.minute_stats = deque(maxlen=60)
|
|
||||||
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(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
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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 _synchronize_portfolio(self, attempt_index=0):
|
|
||||||
try:
|
|
||||||
self.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 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_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:
|
|
||||||
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._synchronize_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()
|
|
||||||
|
|
||||||
minute_stats = self.prepare_period_stats(
|
|
||||||
data.current_dt, data.current_dt + timedelta(minutes=1))
|
|
||||||
# Saving the last hour in memory
|
|
||||||
self.minute_stats.append(minute_stats)
|
|
||||||
|
|
||||||
print_df = pd.DataFrame(list(self.minute_stats))
|
|
||||||
log.debug(
|
|
||||||
'statistics for the last {stats_minutes} minutes:\n{stats}'.format(
|
|
||||||
stats_minutes=self.stats_minutes,
|
|
||||||
stats=get_pretty_stats(print_df, 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))
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
if order_id is not None:
|
|
||||||
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,11 +1,12 @@
|
|||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
log = Logger('AssetFinderExchange')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('AssetFinderExchange', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class AssetFinderExchange(object):
|
class AssetFinderExchange(object):
|
||||||
def __init__(self, exchange):
|
def __init__(self):
|
||||||
self.exchange = exchange
|
|
||||||
self._asset_cache = {}
|
self._asset_cache = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -40,14 +41,15 @@ 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.info('got asset from cache: {}'.format(sid))
|
# log.debug('got asset from cache: {}'.format(sid))
|
||||||
else:
|
# else:
|
||||||
log.info('fetching asset: {}'.format(sid))
|
# log.debug('fetching asset: {}'.format(sid))
|
||||||
return list()
|
return list()
|
||||||
|
|
||||||
def lookup_symbol(self, symbol, as_of_date, fuzzy=False):
|
def lookup_symbol(self, symbol, exchange, data_frequency=None,
|
||||||
|
as_of_date=None, fuzzy=False):
|
||||||
"""Lookup an asset by symbol.
|
"""Lookup an asset by symbol.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -81,11 +83,17 @@ 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))
|
log.debug('looking up symbol: {} {}'.format(symbol, exchange.name))
|
||||||
|
|
||||||
|
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:
|
||||||
asset = self.exchange.get_asset(symbol)
|
key = ','.join([exchange.name, 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,4 +1,5 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
@@ -13,14 +14,16 @@ import six
|
|||||||
from catalyst.assets._assets import TradingPair
|
from catalyst.assets._assets import TradingPair
|
||||||
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)
|
InvalidOrderStyle, OrderCancelError)
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
|
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
|
||||||
ExchangeStopLimitOrder, ExchangeStopOrder
|
ExchangeStopLimitOrder, ExchangeStopOrder
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_symbols_filename, \
|
||||||
|
download_exchange_symbols, get_symbols_string
|
||||||
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
|
||||||
|
|
||||||
@@ -30,7 +33,9 @@ requests.adapters.DEFAULT_RETRIES = 20
|
|||||||
|
|
||||||
BITFINEX_URL = 'https://api.bitfinex.com'
|
BITFINEX_URL = 'https://api.bitfinex.com'
|
||||||
|
|
||||||
log = Logger('Bitfinex')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('Bitfinex', level=LOG_LEVEL)
|
||||||
warning_logger = Logger('AlgoWarning')
|
warning_logger = Logger('AlgoWarning')
|
||||||
|
|
||||||
|
|
||||||
@@ -40,13 +45,29 @@ class Bitfinex(Exchange):
|
|||||||
self.key = key
|
self.key = key
|
||||||
self.secret = secret.encode('UTF-8')
|
self.secret = secret.encode('UTF-8')
|
||||||
self.name = 'bitfinex'
|
self.name = 'bitfinex'
|
||||||
self.assets = {}
|
self.color = 'green'
|
||||||
|
|
||||||
|
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),
|
||||||
@@ -173,6 +194,7 @@ class Bitfinex(Exchange):
|
|||||||
def get_balances(self):
|
def get_balances(self):
|
||||||
log.debug('retrieving wallets balances')
|
log.debug('retrieving wallets balances')
|
||||||
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:
|
||||||
@@ -223,7 +245,8 @@ class Bitfinex(Exchange):
|
|||||||
# TODO: fetch account data and keep in cache
|
# TODO: fetch account data and keep in cache
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_candles(self, data_frequency, assets, bar_count=None):
|
def get_candles(self, freq, assets, bar_count=None,
|
||||||
|
start_dt=None, end_dt=None):
|
||||||
"""
|
"""
|
||||||
Retrieve OHLVC candles from Bitfinex
|
Retrieve OHLVC candles from Bitfinex
|
||||||
|
|
||||||
@@ -237,34 +260,40 @@ 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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: use BcolzMinuteBarReader to read from cache
|
allowed_frequencies = ['1T', '5T', '15T', '30T', '60T', '180T',
|
||||||
freq_match = re.match(r'([0-9].*)(m|h|d)', data_frequency, re.M | re.I)
|
'360T', '720T', '1D', '7D', '14D', '30D']
|
||||||
|
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 == 'd':
|
if unit == 'T':
|
||||||
converted_unit = 'D'
|
if number in [60, 180, 360, 720]:
|
||||||
|
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(
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
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, TradingPair) else assets
|
||||||
@@ -280,11 +309,27 @@ 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)
|
||||||
@@ -298,6 +343,9 @@ 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]),
|
||||||
@@ -305,8 +353,7 @@ 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=pd.Timestamp.utcfromtimestamp(
|
last_traded=last_traded
|
||||||
candle[0] / 1000.0)
|
|
||||||
)
|
)
|
||||||
return ohlc
|
return ohlc
|
||||||
|
|
||||||
@@ -367,6 +414,7 @@ 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()
|
order_status = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -408,6 +456,7 @@ 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:
|
||||||
@@ -419,7 +468,7 @@ class Bitfinex(Exchange):
|
|||||||
order_statuses['message'])
|
order_statuses['message'])
|
||||||
)
|
)
|
||||||
|
|
||||||
orders = list()
|
orders = []
|
||||||
for order_status in order_statuses:
|
for order_status in order_statuses:
|
||||||
order, executed_price = self._create_order(order_status)
|
order, executed_price = self._create_order(order_status)
|
||||||
if asset is None or asset == order.sid:
|
if asset is None or asset == order.sid:
|
||||||
@@ -442,6 +491,7 @@ 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()
|
||||||
@@ -467,6 +517,7 @@ 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:
|
||||||
@@ -491,6 +542,7 @@ 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,
|
||||||
@@ -506,7 +558,10 @@ class Bitfinex(Exchange):
|
|||||||
response.content)
|
response.content)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
tickers = response.json()
|
tickers = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
ticks = dict()
|
ticks = dict()
|
||||||
for index, ticker in enumerate(tickers):
|
for index, ticker in enumerate(tickers):
|
||||||
@@ -527,3 +582,124 @@ class Bitfinex(Exchange):
|
|||||||
|
|
||||||
log.debug('got tickers {}'.format(ticks))
|
log.debug('got tickers {}'.format(ticks))
|
||||||
return ticks
|
return ticks
|
||||||
|
|
||||||
|
def generate_symbols_json(self, filename=None, source_dates=False):
|
||||||
|
symbol_map = {}
|
||||||
|
|
||||||
|
if not source_dates:
|
||||||
|
fn, r = download_exchange_symbols(self.name)
|
||||||
|
with open(fn) as data_file:
|
||||||
|
cached_symbols = json.load(data_file)
|
||||||
|
|
||||||
|
response = self._request('symbols', None)
|
||||||
|
|
||||||
|
for symbol in response.json():
|
||||||
|
if (source_dates):
|
||||||
|
start_date = self.get_symbol_start_date(symbol)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
start_date = cached_symbols[symbol]['start_date']
|
||||||
|
except KeyError as e:
|
||||||
|
start_date = time.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_daily = cached_symbols[symbol]['end_daily']
|
||||||
|
except KeyError as e:
|
||||||
|
end_daily = 'N/A'
|
||||||
|
|
||||||
|
try:
|
||||||
|
end_minute = cached_symbols[symbol]['end_minute']
|
||||||
|
except KeyError as e:
|
||||||
|
end_minute = 'N/A'
|
||||||
|
|
||||||
|
symbol_map[symbol] = dict(
|
||||||
|
symbol=symbol[:-3] + '_' + symbol[-3:],
|
||||||
|
start_date=start_date,
|
||||||
|
end_daily=end_daily,
|
||||||
|
end_minute=end_minute,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (filename is None):
|
||||||
|
filename = get_exchange_symbols_filename(self.name)
|
||||||
|
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(symbol_map, f, sort_keys=True, indent=2,
|
||||||
|
separators=(',', ':'))
|
||||||
|
|
||||||
|
def get_symbol_start_date(self, symbol):
|
||||||
|
|
||||||
|
print(symbol)
|
||||||
|
symbol_v2 = 't' + symbol.upper()
|
||||||
|
|
||||||
|
"""
|
||||||
|
For each symbol we retrieve candles with Monhtly resolution
|
||||||
|
We get the first month, and query again with daily resolution
|
||||||
|
around that date, and we get the first date
|
||||||
|
"""
|
||||||
|
url = '{url}/v2/candles/trade:1M:{symbol}/hist'.format(
|
||||||
|
url=self.url,
|
||||||
|
symbol=symbol_v2
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = requests.get(url)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
"""
|
||||||
|
If we don't get any data back for our monthly-resolution query
|
||||||
|
it means that symbol started trading less than a month ago, so
|
||||||
|
arbitrarily set the ref. date to 15 days ago to be safe with
|
||||||
|
+/- 31 days
|
||||||
|
"""
|
||||||
|
if (len(response.json())):
|
||||||
|
startmonth = response.json()[-1][0]
|
||||||
|
else:
|
||||||
|
startmonth = int((time.time() - 15 * 24 * 3600) * 1000)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Query again with daily resolution setting the start and end around
|
||||||
|
the startmonth we got above. Avoid end dates greater than now: time.time()
|
||||||
|
"""
|
||||||
|
url = '{url}/v2/candles/trade:1D:{symbol}/hist?start={start}&end={end}'.format(
|
||||||
|
url=self.url,
|
||||||
|
symbol=symbol_v2,
|
||||||
|
start=startmonth - 3600 * 24 * 31 * 1000,
|
||||||
|
end=min(startmonth + 3600 * 24 * 31 * 1000,
|
||||||
|
int(time.time() * 1000))
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
response = requests.get(url)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
return time.strftime('%Y-%m-%d',
|
||||||
|
time.gmtime(int(response.json()[-1][0] / 1000)))
|
||||||
|
|
||||||
|
def get_orderbook(self, asset, order_type='all', limit=100):
|
||||||
|
exchange_symbol = asset.exchange_symbol
|
||||||
|
try:
|
||||||
|
self.ask_request()
|
||||||
|
# TODO: implement limit
|
||||||
|
response = self._request(
|
||||||
|
'book/{}'.format(exchange_symbol), None)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
# TODO: filter by type
|
||||||
|
result = dict()
|
||||||
|
for order_type in data:
|
||||||
|
result[order_type] = []
|
||||||
|
|
||||||
|
for entry in data[order_type]:
|
||||||
|
result[order_type].append(dict(
|
||||||
|
rate=float(entry['price']),
|
||||||
|
quantity=float(entry['amount'])
|
||||||
|
))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
{
|
{
|
||||||
|
"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"
|
||||||
@@ -17,19 +30,19 @@
|
|||||||
},
|
},
|
||||||
"ethusd": {
|
"ethusd": {
|
||||||
"symbol": "eth_usd",
|
"symbol": "eth_usd",
|
||||||
"start_date": "2010-01-01"
|
"start_date": "2017-01-01"
|
||||||
},
|
},
|
||||||
"ethbtc": {
|
"ethbtc": {
|
||||||
"symbol": "eth_btc",
|
"symbol": "eth_btc",
|
||||||
"start_date": "2010-01-01"
|
"start_date": "2017-01-01"
|
||||||
},
|
},
|
||||||
"etcbtc": {
|
"etcbtc": {
|
||||||
"symbol": "etc_btc",
|
"symbol": "etc_btc",
|
||||||
"start_date": "2010-01-01"
|
"start_date": "2017-01-01"
|
||||||
},
|
},
|
||||||
"etcusd": {
|
"etcusd": {
|
||||||
"symbol": "etc_usd",
|
"symbol": "etc_usd",
|
||||||
"start_date": "2010-01-01"
|
"start_date": "2017-01-01"
|
||||||
},
|
},
|
||||||
"rrtusd": {
|
"rrtusd": {
|
||||||
"symbol": "rrt_usd",
|
"symbol": "rrt_usd",
|
||||||
|
|||||||
@@ -1,36 +1,56 @@
|
|||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from catalyst.assets._assets import TradingPair
|
from catalyst.assets._assets import TradingPair
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
from six.moves import urllib
|
from six.moves import urllib
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.exchange.bittrex.bittrex_api import Bittrex_api
|
from catalyst.exchange.bittrex.bittrex_api import Bittrex_api
|
||||||
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 InvalidHistoryFrequencyError, \
|
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||||
ExchangeRequestError, InvalidOrderStyle, OrderNotFound, OrderCancelError, \
|
ExchangeRequestError, InvalidOrderStyle, OrderNotFound, OrderCancelError, \
|
||||||
CreateOrderError
|
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.execution import LimitOrder, StopLimitOrder
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
|
|
||||||
log = Logger('Bittrex')
|
# TODO: consider using this: https://github.com/mondeja/bittrex_v2
|
||||||
|
|
||||||
|
log = Logger('Bittrex', level=LOG_LEVEL)
|
||||||
|
|
||||||
URL2 = 'https://bittrex.com/Api/v2.0'
|
URL2 = 'https://bittrex.com/Api/v2.0'
|
||||||
|
|
||||||
|
|
||||||
class Bittrex(Exchange):
|
class Bittrex(Exchange):
|
||||||
def __init__(self, key, secret, base_currency, portfolio=None):
|
def __init__(self, key, secret, base_currency, portfolio=None):
|
||||||
self.api = Bittrex_api(key=key, secret=secret.encode('UTF-8'))
|
self.api = Bittrex_api(key=key, secret=secret)
|
||||||
self.name = 'bittrex'
|
self.name = 'bittrex'
|
||||||
|
self.color = 'blue'
|
||||||
self.base_currency = base_currency
|
self.base_currency = base_currency
|
||||||
self._portfolio = portfolio
|
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_writer = None
|
||||||
self.minute_reader = None
|
self.minute_reader = None
|
||||||
|
|
||||||
self.assets = dict()
|
self.assets = dict()
|
||||||
self.load_assets()
|
self.load_assets()
|
||||||
|
|
||||||
|
self.local_assets = dict()
|
||||||
|
self.load_assets(is_local=True)
|
||||||
|
|
||||||
|
self.bundle = ExchangeBundle(self.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account(self):
|
def account(self):
|
||||||
pass
|
pass
|
||||||
@@ -50,42 +70,24 @@ class Bittrex(Exchange):
|
|||||||
"""
|
"""
|
||||||
return exchange_symbol.lower()
|
return exchange_symbol.lower()
|
||||||
|
|
||||||
def fetch_symbol_map(self):
|
|
||||||
"""
|
|
||||||
Since Bittrex gives us a complete dictionary of symbols,
|
|
||||||
we can build the symbol map ad-hoc as opposed to maintaining
|
|
||||||
a static file. We must be careful with mapping any unconventional
|
|
||||||
symbol name as appropriate.
|
|
||||||
|
|
||||||
:return symbol_map:
|
|
||||||
"""
|
|
||||||
symbol_map = dict()
|
|
||||||
|
|
||||||
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'])
|
|
||||||
)
|
|
||||||
symbol_map[exchange_symbol] = dict(
|
|
||||||
symbol=symbol,
|
|
||||||
start_date=pd.to_datetime(market['Created'], utc=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
return symbol_map
|
|
||||||
|
|
||||||
def get_balances(self):
|
def get_balances(self):
|
||||||
|
balances = self.api.getbalances()
|
||||||
try:
|
try:
|
||||||
log.debug('retrieving wallet balances')
|
log.debug('retrieving wallet balances')
|
||||||
balances = self.api.getbalances()
|
self.ask_request()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
std_balances = dict()
|
std_balances = dict()
|
||||||
|
try:
|
||||||
for balance in balances:
|
for balance in balances:
|
||||||
currency = balance['Currency'].lower()
|
currency = balance['Currency'].lower()
|
||||||
std_balances[currency] = balance['Available']
|
std_balances[currency] = balance['Available']
|
||||||
|
|
||||||
|
except TypeError:
|
||||||
|
raise ExchangeRequestError(error=balances)
|
||||||
|
|
||||||
return std_balances
|
return std_balances
|
||||||
|
|
||||||
def create_order(self, asset, amount, is_buy, style):
|
def create_order(self, asset, amount, is_buy, style):
|
||||||
@@ -98,6 +100,7 @@ class Bittrex(Exchange):
|
|||||||
|
|
||||||
price = style.get_limit_price(is_buy)
|
price = style.get_limit_price(is_buy)
|
||||||
try:
|
try:
|
||||||
|
self.ask_request()
|
||||||
if is_buy:
|
if is_buy:
|
||||||
order_status = self.api.buylimit(exchange_symbol, amount,
|
order_status = self.api.buylimit(exchange_symbol, amount,
|
||||||
price)
|
price)
|
||||||
@@ -119,7 +122,18 @@ class Bittrex(Exchange):
|
|||||||
)
|
)
|
||||||
return order
|
return order
|
||||||
else:
|
else:
|
||||||
raise CreateOrderError(exchange=self.name, error=order_status)
|
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:
|
else:
|
||||||
raise InvalidOrderStyle(exchange=self.name,
|
raise InvalidOrderStyle(exchange=self.name,
|
||||||
style=style.__class__.__name__)
|
style=style.__class__.__name__)
|
||||||
@@ -127,6 +141,7 @@ class Bittrex(Exchange):
|
|||||||
def get_open_orders(self, asset):
|
def get_open_orders(self, asset):
|
||||||
symbol = self.get_symbol(asset)
|
symbol = self.get_symbol(asset)
|
||||||
try:
|
try:
|
||||||
|
self.ask_request()
|
||||||
open_orders = self.api.getopenorders(symbol)
|
open_orders = self.api.getopenorders(symbol)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
@@ -170,6 +185,7 @@ class Bittrex(Exchange):
|
|||||||
def get_order(self, order_id):
|
def get_order(self, order_id):
|
||||||
log.info('retrieving order {}'.format(order_id))
|
log.info('retrieving order {}'.format(order_id))
|
||||||
try:
|
try:
|
||||||
|
self.ask_request()
|
||||||
order_status = self.api.getorder(order_id)
|
order_status = self.api.getorder(order_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
@@ -185,6 +201,7 @@ class Bittrex(Exchange):
|
|||||||
log.info('cancelling order {}'.format(order_id))
|
log.info('cancelling order {}'.format(order_id))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self.ask_request()
|
||||||
status = self.api.cancel(order_id)
|
status = self.api.cancel(order_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
@@ -196,43 +213,59 @@ class Bittrex(Exchange):
|
|||||||
error=status['message']
|
error=status['message']
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_candles(self, data_frequency, assets, bar_count=None):
|
def get_candles(self, freq, assets, bar_count=None,
|
||||||
|
start_dt=None, end_dt=None):
|
||||||
"""
|
"""
|
||||||
Supported Intervals
|
Supported Intervals
|
||||||
-------------------
|
-------------------
|
||||||
day, oneMin, fiveMin, thirtyMin, hour
|
day, oneMin, fiveMin, thirtyMin, hour
|
||||||
|
|
||||||
:param data_frequency:
|
:param freq:
|
||||||
:param assets:
|
:param assets:
|
||||||
:param bar_count:
|
:param bar_count:
|
||||||
|
:param start_dt
|
||||||
|
:param end_dt
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
log.info('retrieving candles')
|
|
||||||
|
|
||||||
if data_frequency == 'minute' or data_frequency == '1m':
|
# 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'
|
frequency = 'oneMin'
|
||||||
elif data_frequency == '5m':
|
elif freq == '5T':
|
||||||
frequency = 'fiveMin'
|
frequency = 'fiveMin'
|
||||||
elif data_frequency == '30m':
|
elif freq == '30T':
|
||||||
frequency = 'thirtyMin'
|
frequency = 'thirtyMin'
|
||||||
elif data_frequency == '1h':
|
elif freq == '60T':
|
||||||
frequency = 'hour'
|
frequency = 'hour'
|
||||||
elif data_frequency == 'daily' or data_frequency == '1D':
|
elif freq == '1D':
|
||||||
frequency = 'day'
|
frequency = 'day'
|
||||||
else:
|
else:
|
||||||
raise InvalidHistoryFrequencyError(
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
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, TradingPair) else assets
|
||||||
ohlc_map = dict()
|
|
||||||
for asset in asset_list:
|
for asset in asset_list:
|
||||||
|
end = int(time.mktime(end_dt.timetuple()))
|
||||||
url = '{url}/pub/market/GetTicks?marketName={symbol}' \
|
url = '{url}/pub/market/GetTicks?marketName={symbol}' \
|
||||||
'&tickInterval={frequency}&_=1499127220008'.format(
|
'&tickInterval={frequency}&_={end}'.format(
|
||||||
url=URL2,
|
url=URL2,
|
||||||
symbol=self.get_symbol(asset),
|
symbol=self.get_symbol(asset),
|
||||||
frequency=frequency
|
frequency=frequency,
|
||||||
|
end=end
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -260,9 +293,11 @@ class Bittrex(Exchange):
|
|||||||
return ohlc
|
return ohlc
|
||||||
|
|
||||||
ordered_candles = list(reversed(candles))
|
ordered_candles = list(reversed(candles))
|
||||||
|
ohlc_map = dict()
|
||||||
if bar_count is None:
|
if bar_count is None:
|
||||||
ohlc_map[asset] = ohlc_from_candle(ordered_candles[0])
|
ohlc_map[asset] = ohlc_from_candle(ordered_candles[0])
|
||||||
else:
|
else:
|
||||||
|
# TODO: optimize
|
||||||
ohlc_bars = []
|
ohlc_bars = []
|
||||||
for candle in ordered_candles[:bar_count]:
|
for candle in ordered_candles[:bar_count]:
|
||||||
ohlc = ohlc_from_candle(candle)
|
ohlc = ohlc_from_candle(candle)
|
||||||
@@ -287,6 +322,7 @@ class Bittrex(Exchange):
|
|||||||
for asset in assets:
|
for asset in assets:
|
||||||
symbol = self.get_symbol(asset)
|
symbol = self.get_symbol(asset)
|
||||||
try:
|
try:
|
||||||
|
self.ask_request()
|
||||||
ticker = self.api.getticker(symbol)
|
ticker = self.api.getticker(symbol)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
@@ -305,3 +341,76 @@ class Bittrex(Exchange):
|
|||||||
def get_account(self):
|
def get_account(self):
|
||||||
log.info('retrieving account data')
|
log.info('retrieving account data')
|
||||||
pass
|
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
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import json
|
|||||||
import time
|
import time
|
||||||
import hmac
|
import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import ssl
|
||||||
from six.moves import urllib
|
|
||||||
|
|
||||||
# Workaround for backwards compatibility
|
# Workaround for backwards compatibility
|
||||||
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
|
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
urlopen = urllib.request.urlopen
|
urlopen = urllib.request.urlopen
|
||||||
|
|
||||||
|
|
||||||
@@ -39,13 +40,17 @@ class Bittrex_api(object):
|
|||||||
if method not in self.public:
|
if method not in self.public:
|
||||||
url += '&apikey=' + self.key
|
url += '&apikey=' + self.key
|
||||||
url += '&nonce=' + str(int(time.time()))
|
url += '&nonce=' + str(int(time.time()))
|
||||||
signature = hmac.new(self.secret, url, hashlib.sha512).hexdigest()
|
|
||||||
|
signature = hmac.new(self.secret.encode('utf-8'),
|
||||||
|
url.encode('utf-8'),
|
||||||
|
hashlib.sha512).hexdigest()
|
||||||
headers = {'apisign': signature}
|
headers = {'apisign': signature}
|
||||||
else:
|
else:
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
req = urllib.request.Request(url, headers=headers)
|
req = urllib.request.Request(url, headers=headers)
|
||||||
response = json.loads(urlopen(req).read())
|
response = json.loads(urlopen(
|
||||||
|
req, context=ssl._create_unverified_context()).read())
|
||||||
|
|
||||||
if response["result"]:
|
if response["result"]:
|
||||||
return response["result"]
|
return response["result"]
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from catalyst.data.bundles import register
|
||||||
|
from catalyst.exchange.exchange_bundle import exchange_bundle
|
||||||
|
|
||||||
|
symbols = (
|
||||||
|
'neo_btc',
|
||||||
|
)
|
||||||
|
register('exchange_bitfinex', exchange_bundle('bitfinex', symbols))
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
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,121 +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.
|
|
||||||
|
|
||||||
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!")
|
|
||||||
+504
-136
@@ -1,7 +1,6 @@
|
|||||||
import abc
|
import abc
|
||||||
import collections
|
|
||||||
import random
|
|
||||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||||
|
from datetime import timedelta
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -9,20 +8,24 @@ import pandas as pd
|
|||||||
from catalyst.assets._assets import TradingPair
|
from catalyst.assets._assets import TradingPair
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.data.data_portal import BASE_FIELDS
|
from catalyst.data.data_portal import BASE_FIELDS
|
||||||
from catalyst.errors import (
|
from catalyst.exchange.bundle_utils import get_start_dt, \
|
||||||
SymbolNotFound,
|
get_delta, get_periods, get_periods_range
|
||||||
)
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
||||||
InvalidOrderStyle, BaseCurrencyNotFoundError
|
InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \
|
||||||
|
PricingDataNotLoadedError, \
|
||||||
|
NoDataAvailableOnExchange, ExchangeSymbolsNotFound
|
||||||
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
|
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
|
||||||
ExchangeLimitOrder, ExchangeStopOrder
|
ExchangeLimitOrder, ExchangeStopOrder
|
||||||
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols
|
from catalyst.exchange.exchange_utils import get_exchange_symbols, \
|
||||||
|
get_frequency, resample_history_df
|
||||||
from catalyst.finance.order import ORDER_STATUS
|
from catalyst.finance.order import ORDER_STATUS
|
||||||
from catalyst.finance.transaction import Transaction
|
from catalyst.finance.transaction import Transaction
|
||||||
|
|
||||||
log = Logger('Exchange')
|
log = Logger('Exchange', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class Exchange:
|
class Exchange:
|
||||||
@@ -30,13 +33,18 @@ class Exchange:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.name = None
|
self.name = None
|
||||||
self.trading_pairs = None
|
self.assets = dict()
|
||||||
self.assets = {}
|
self.local_assets = dict()
|
||||||
self._portfolio = None
|
self._portfolio = None
|
||||||
self.minute_writer = None
|
self.minute_writer = None
|
||||||
self.minute_reader = None
|
self.minute_reader = None
|
||||||
self.base_currency = None
|
self.base_currency = None
|
||||||
|
|
||||||
|
self.num_candles_limit = None
|
||||||
|
self.max_requests_per_minute = None
|
||||||
|
self.request_cpt = None
|
||||||
|
self.bundle = ExchangeBundle(self.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def positions(self):
|
def positions(self):
|
||||||
return self.portfolio.positions
|
return self.portfolio.positions
|
||||||
@@ -44,9 +52,11 @@ class Exchange:
|
|||||||
@property
|
@property
|
||||||
def portfolio(self):
|
def portfolio(self):
|
||||||
"""
|
"""
|
||||||
Return the Portfolio
|
The exchange portfolio
|
||||||
|
|
||||||
:return:
|
Returns
|
||||||
|
-------
|
||||||
|
ExchangePortfolio
|
||||||
"""
|
"""
|
||||||
if self._portfolio is None:
|
if self._portfolio is None:
|
||||||
self._portfolio = ExchangePortfolio(
|
self._portfolio = ExchangePortfolio(
|
||||||
@@ -64,12 +74,74 @@ class Exchange:
|
|||||||
def time_skew(self):
|
def time_skew(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def is_open(self, dt):
|
||||||
|
"""
|
||||||
|
Is the exchange open
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dt: Timestamp
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: implement for each exchange.
|
||||||
|
return True
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
now = pd.Timestamp.utcnow()
|
||||||
|
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 + timedelta(minutes=1):
|
||||||
|
self.request_cpt = dict()
|
||||||
|
self.request_cpt[now] = 0
|
||||||
|
return True
|
||||||
|
|
||||||
|
if cpt >= self.max_requests_per_minute:
|
||||||
|
delta = now - cpt_date
|
||||||
|
|
||||||
|
sleep_period = 60 - delta.total_seconds()
|
||||||
|
sleep(sleep_period)
|
||||||
|
|
||||||
|
now = pd.Timestamp.utcnow()
|
||||||
|
self.request_cpt = dict()
|
||||||
|
self.request_cpt[now] = 0
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.request_cpt[cpt_date] += 1
|
||||||
|
|
||||||
def get_symbol(self, asset):
|
def get_symbol(self, asset):
|
||||||
"""
|
"""
|
||||||
Get the exchange specific symbol of the given asset.
|
The exchange specific symbol of the specified market.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset: TradingPair
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
:param asset: Asset
|
|
||||||
:return: symbol: str
|
|
||||||
"""
|
"""
|
||||||
symbol = None
|
symbol = None
|
||||||
|
|
||||||
@@ -79,7 +151,7 @@ class Exchange:
|
|||||||
|
|
||||||
if not symbol:
|
if not symbol:
|
||||||
raise ValueError('Currency %s not supported by exchange %s' %
|
raise ValueError('Currency %s not supported by exchange %s' %
|
||||||
(asset['symbol'], self.name))
|
(asset['symbol'], self.name.title()))
|
||||||
|
|
||||||
return symbol
|
return symbol
|
||||||
|
|
||||||
@@ -87,55 +159,122 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Get a list of symbols corresponding to each given asset.
|
Get a list of symbols corresponding to each given asset.
|
||||||
|
|
||||||
:param assets: Asset[]
|
Parameters
|
||||||
:return:
|
----------
|
||||||
|
assets: list[TradingPair]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[str]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
symbols = []
|
symbols = []
|
||||||
|
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
symbols.append(self.get_symbol(asset))
|
symbols.append(self.get_symbol(asset))
|
||||||
|
|
||||||
return symbols
|
return symbols
|
||||||
|
|
||||||
def get_asset(self, symbol):
|
def get_assets(self, symbols=None, data_frequency=None):
|
||||||
"""
|
"""
|
||||||
Find an Asset on the current exchange based on its Catalyst symbol
|
The list of markets for the specified symbols.
|
||||||
:param symbol: the [target]_[base] currency pair symbol
|
|
||||||
:return: Asset
|
|
||||||
"""
|
|
||||||
asset = None
|
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
symbols: list[str]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[TradingPair]
|
||||||
|
|
||||||
|
"""
|
||||||
|
assets = []
|
||||||
|
|
||||||
|
if symbols is not None:
|
||||||
|
for symbol in symbols:
|
||||||
|
asset = self.get_asset(symbol, data_frequency)
|
||||||
|
assets.append(asset)
|
||||||
|
else:
|
||||||
for key in self.assets:
|
for key in self.assets:
|
||||||
if not asset and self.assets[key].symbol.lower() == symbol.lower():
|
assets.append(self.assets[key])
|
||||||
asset = self.assets[key]
|
|
||||||
|
|
||||||
if not asset:
|
return assets
|
||||||
raise SymbolNotFound(symbol=symbol)
|
|
||||||
|
def _find_asset(self, asset, symbol, data_frequency, is_local=False):
|
||||||
|
assets = self.assets if not is_local else self.local_assets
|
||||||
|
|
||||||
|
for key in assets:
|
||||||
|
has_data = (data_frequency == 'minute'
|
||||||
|
and assets[key].end_minute is not None) \
|
||||||
|
or (data_frequency == 'daily'
|
||||||
|
and assets[key].end_daily is not None)
|
||||||
|
if not asset and assets[key].symbol.lower() == symbol.lower() \
|
||||||
|
and (not data_frequency or has_data):
|
||||||
|
asset = assets[key]
|
||||||
|
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
def fetch_symbol_map(self):
|
def get_asset(self, symbol, data_frequency=None):
|
||||||
return get_exchange_symbols(self.name)
|
"""
|
||||||
|
The market for the specified symbol.
|
||||||
|
|
||||||
def load_assets(self):
|
Parameters
|
||||||
|
----------
|
||||||
|
symbol: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
TradingPair
|
||||||
|
|
||||||
|
"""
|
||||||
|
asset = None
|
||||||
|
|
||||||
|
log.debug('searching asset {} on the server'.format(symbol))
|
||||||
|
asset = self._find_asset(asset, symbol, data_frequency, False)
|
||||||
|
|
||||||
|
log.debug('asset {} not found on the server, searching local '
|
||||||
|
'assets'.format(symbol))
|
||||||
|
asset = self._find_asset(asset, symbol, data_frequency, True)
|
||||||
|
|
||||||
|
if not asset:
|
||||||
|
all_values = list(self.assets.values()) + \
|
||||||
|
list(self.local_assets.values())
|
||||||
|
supported_symbols = sorted([
|
||||||
|
asset.symbol for asset in all_values
|
||||||
|
])
|
||||||
|
|
||||||
|
raise SymbolNotFoundOnExchange(
|
||||||
|
symbol=symbol,
|
||||||
|
exchange=self.name.title(),
|
||||||
|
supported_symbols=supported_symbols
|
||||||
|
)
|
||||||
|
|
||||||
|
return asset
|
||||||
|
|
||||||
|
def fetch_symbol_map(self, is_local=False):
|
||||||
|
return get_exchange_symbols(self.name, is_local)
|
||||||
|
|
||||||
|
def load_assets(self, is_local=False):
|
||||||
"""
|
"""
|
||||||
Populate the 'assets' attribute with a dictionary of Assets.
|
Populate the 'assets' attribute with a dictionary of Assets.
|
||||||
The key of the resulting dictionary is the exchange specific
|
The key of the resulting dictionary is the exchange specific
|
||||||
currency pair symbol. The universal symbol is contained in the
|
currency pair symbol. The universal symbol is contained in the
|
||||||
'symbol' attribute of each asset.
|
'symbol' attribute of each asset.
|
||||||
|
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
The sid of each asset is calculated based on a numeric hash of the
|
The sid of each asset is calculated based on a numeric hash of the
|
||||||
universal symbol. This simple approach avoids maintaining a mapping
|
universal symbol. This simple approach avoids maintaining a mapping
|
||||||
of sids.
|
of sids.
|
||||||
|
|
||||||
This method can be overridden if an exchange offers equivalent data
|
This method can be omerridden if an exchange offers equivalent data
|
||||||
via its api.
|
via its api.
|
||||||
"""
|
|
||||||
|
|
||||||
symbol_map = self.fetch_symbol_map()
|
"""
|
||||||
|
try:
|
||||||
|
symbol_map = self.fetch_symbol_map(is_local)
|
||||||
|
except ExchangeSymbolsNotFound:
|
||||||
|
return None
|
||||||
|
|
||||||
for exchange_symbol in symbol_map:
|
for exchange_symbol in symbol_map:
|
||||||
asset = symbol_map[exchange_symbol]
|
asset = symbol_map[exchange_symbol]
|
||||||
|
|
||||||
@@ -159,15 +298,37 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
asset_name = None
|
asset_name = None
|
||||||
|
|
||||||
|
if 'min_trade_size' in asset:
|
||||||
|
min_trade_size = asset['min_trade_size']
|
||||||
|
else:
|
||||||
|
min_trade_size = 0.0000001
|
||||||
|
|
||||||
|
if 'end_daily' in asset and asset['end_daily'] != 'N/A':
|
||||||
|
end_daily = pd.to_datetime(asset['end_daily'], utc=True)
|
||||||
|
else:
|
||||||
|
end_daily = None
|
||||||
|
|
||||||
|
if 'end_minute' in asset and asset['end_minute'] != 'N/A':
|
||||||
|
end_minute = pd.to_datetime(asset['end_minute'], utc=True)
|
||||||
|
else:
|
||||||
|
end_minute = None
|
||||||
|
|
||||||
trading_pair = TradingPair(
|
trading_pair = TradingPair(
|
||||||
symbol=asset['symbol'],
|
symbol=asset['symbol'],
|
||||||
exchange=self.name,
|
exchange=self.name,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
leverage=leverage,
|
leverage=leverage,
|
||||||
asset_name=asset_name
|
asset_name=asset_name,
|
||||||
|
min_trade_size=min_trade_size,
|
||||||
|
end_daily=end_daily,
|
||||||
|
end_minute=end_minute,
|
||||||
|
exchange_symbol=exchange_symbol
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if is_local:
|
||||||
|
self.local_assets[exchange_symbol] = trading_pair
|
||||||
|
else:
|
||||||
self.assets[exchange_symbol] = trading_pair
|
self.assets[exchange_symbol] = trading_pair
|
||||||
|
|
||||||
def check_open_orders(self):
|
def check_open_orders(self):
|
||||||
@@ -176,8 +337,10 @@ class Exchange:
|
|||||||
For each executed order found, create a transaction and apply to the
|
For each executed order found, create a transaction and apply to the
|
||||||
Portfolio.
|
Portfolio.
|
||||||
|
|
||||||
:return:
|
Returns
|
||||||
transactions: Transaction[]
|
-------
|
||||||
|
list[Transaction]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
transactions = list()
|
transactions = list()
|
||||||
if self.portfolio.open_orders:
|
if self.portfolio.open_orders:
|
||||||
@@ -247,35 +410,37 @@ class Exchange:
|
|||||||
'1D', '7D', '14D', '1M'
|
'1D', '7D', '14D', '1M'
|
||||||
"""
|
"""
|
||||||
if field not in BASE_FIELDS:
|
if field not in BASE_FIELDS:
|
||||||
raise KeyError('Invalid column: ' + str(field))
|
raise KeyError('Invalid column: {}'.format(field))
|
||||||
|
|
||||||
if isinstance(assets, collections.Iterable):
|
values = []
|
||||||
values = list()
|
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
value = self.get_single_spot_value(
|
value = self.get_single_spot_value(asset, field, data_frequency)
|
||||||
asset, field, data_frequency)
|
|
||||||
values.append(value)
|
values.append(value)
|
||||||
|
|
||||||
return values
|
return values
|
||||||
else:
|
|
||||||
return self.get_single_spot_value(
|
|
||||||
assets, field, data_frequency)
|
|
||||||
|
|
||||||
def get_single_spot_value(self, asset, field, data_frequency):
|
def get_single_spot_value(self, asset, field, data_frequency):
|
||||||
"""
|
"""
|
||||||
Similar to 'get_spot_value' but for a single asset
|
Similar to 'get_spot_value' but for a single asset
|
||||||
|
|
||||||
Note
|
Notes
|
||||||
----
|
-----
|
||||||
We're writing each minute bar to disk using zipline's machinery.
|
We're writing each minute bar to disk using zipline's machinery.
|
||||||
This is especially useful when running multiple algorithms
|
This is especially useful when running multiple algorithms
|
||||||
concurrently. By using local data when possible, we try to reaching
|
concurrently. By using local data when possible, we try to reaching
|
||||||
request limits on exchanges.
|
request limits on exchanges.
|
||||||
|
|
||||||
:param asset:
|
Parameters
|
||||||
:param field:
|
----------
|
||||||
:param data_frequency:
|
asset: TradingPair
|
||||||
:return value: The spot value of the given asset / field
|
field: str
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
The spot value of the given asset / field
|
||||||
|
|
||||||
"""
|
"""
|
||||||
log.debug(
|
log.debug(
|
||||||
'fetching spot value {field} for symbol {symbol}'.format(
|
'fetching spot value {field} for symbol {symbol}'.format(
|
||||||
@@ -284,64 +449,58 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if field == 'price':
|
freq = '1T' if data_frequency == 'minute' else '1D'
|
||||||
field = 'close'
|
ohlc = self.get_candles(freq, asset)
|
||||||
|
|
||||||
# Don't use a timezone here
|
|
||||||
dt = pd.Timestamp.utcnow().floor('1 min')
|
|
||||||
value = None
|
|
||||||
if self.minute_reader is not None:
|
|
||||||
try:
|
|
||||||
# Slight delay to minimize the chances that multiple algos
|
|
||||||
# might try to hit the cache at the exact same time.
|
|
||||||
sleep_time = random.uniform(0.5, 0.8)
|
|
||||||
sleep(sleep_time)
|
|
||||||
# TODO: This does not always! Why is that? Open an issue with zipline.
|
|
||||||
# See: https://github.com/zipline-live/zipline/issues/26
|
|
||||||
value = self.minute_reader.get_value(
|
|
||||||
sid=asset.sid,
|
|
||||||
dt=dt,
|
|
||||||
field=field
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log.warn('minute data not found: {}'.format(e))
|
|
||||||
|
|
||||||
if value is None or np.isnan(value):
|
|
||||||
ohlc = self.get_candles(data_frequency, asset)
|
|
||||||
if field not in ohlc:
|
if field not in ohlc:
|
||||||
raise KeyError('Invalid column: %s' % field)
|
raise KeyError('Invalid column: %s' % field)
|
||||||
|
|
||||||
if self.minute_writer is not None:
|
|
||||||
df = pd.DataFrame(
|
|
||||||
[ohlc],
|
|
||||||
index=pd.DatetimeIndex([dt]),
|
|
||||||
columns=['open', 'high', 'low', 'close', 'volume']
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.minute_writer.write_sid(
|
|
||||||
sid=asset.sid,
|
|
||||||
df=df
|
|
||||||
)
|
|
||||||
log.debug('wrote minute data: {}'.format(dt))
|
|
||||||
except Exception as e:
|
|
||||||
log.warn(
|
|
||||||
'unable to write minute data: {} {}'.format(dt, e))
|
|
||||||
|
|
||||||
value = ohlc[field]
|
value = ohlc[field]
|
||||||
log.debug('got spot value: {}'.format(value))
|
log.debug('got spot value: {}'.format(value))
|
||||||
else:
|
|
||||||
log.debug('got spot value from cache: {}'.format(value))
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def get_series_from_candles(self, candles, start_dt, end_dt,
|
||||||
|
data_frequency, field, previous_value=None):
|
||||||
|
"""
|
||||||
|
Get a series of field data for the specified candles.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
candles: list[dict[str, float]]
|
||||||
|
start_dt: datetime
|
||||||
|
end_dt: datetime
|
||||||
|
data_frequency: str
|
||||||
|
field: str
|
||||||
|
previous_value: float
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Series
|
||||||
|
|
||||||
|
"""
|
||||||
|
dates = [candle['last_traded'] for candle in candles]
|
||||||
|
values = [candle[field] for candle in candles]
|
||||||
|
series = pd.Series(values, index=dates)
|
||||||
|
|
||||||
|
periods = get_periods_range(
|
||||||
|
start_dt, end_dt, data_frequency
|
||||||
|
)
|
||||||
|
# TODO: ensure that this working as expected, if not use fillna
|
||||||
|
series = series.reindex(
|
||||||
|
periods,
|
||||||
|
method='ffill',
|
||||||
|
fill_value=previous_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
return series
|
||||||
|
|
||||||
def get_history_window(self,
|
def get_history_window(self,
|
||||||
assets,
|
assets,
|
||||||
end_dt,
|
end_dt,
|
||||||
bar_count,
|
bar_count,
|
||||||
frequency,
|
frequency,
|
||||||
field,
|
field,
|
||||||
data_frequency,
|
data_frequency=None,
|
||||||
ffill=True):
|
ffill=True):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -350,10 +509,11 @@ class Exchange:
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
assets : list of catalyst.data.Asset objects
|
assets : list[TradingPair]
|
||||||
The assets whose data is desired.
|
The assets whose data is desired.
|
||||||
|
|
||||||
end_dt: not applicable to cryptocurrencies
|
end_dt: datetime
|
||||||
|
The date of the last bar
|
||||||
|
|
||||||
bar_count: int
|
bar_count: int
|
||||||
The number of bars desired.
|
The number of bars desired.
|
||||||
@@ -375,26 +535,151 @@ class Exchange:
|
|||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
DataFrame
|
||||||
A dataframe containing the requested data.
|
A dataframe containing the requested data.
|
||||||
"""
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
freq, candle_size, unit, data_frequency = get_frequency(
|
||||||
|
frequency, data_frequency
|
||||||
|
)
|
||||||
|
adj_bar_count = candle_size * bar_count
|
||||||
|
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
|
||||||
|
|
||||||
|
# The get_history method supports multiple asset
|
||||||
candles = self.get_candles(
|
candles = self.get_candles(
|
||||||
data_frequency=frequency,
|
freq=freq,
|
||||||
assets=assets,
|
assets=assets,
|
||||||
bar_count=bar_count,
|
bar_count=bar_count,
|
||||||
|
start_dt=start_dt,
|
||||||
|
end_dt=end_dt
|
||||||
)
|
)
|
||||||
|
|
||||||
series = dict()
|
series = dict()
|
||||||
|
for asset in candles:
|
||||||
|
asset_series = self.get_series_from_candles(
|
||||||
|
candles=candles[asset],
|
||||||
|
start_dt=start_dt,
|
||||||
|
end_dt=end_dt,
|
||||||
|
data_frequency=frequency,
|
||||||
|
field=field,
|
||||||
|
)
|
||||||
|
series[asset] = asset_series
|
||||||
|
|
||||||
|
df = pd.DataFrame(series)
|
||||||
|
df.dropna(inplace=True)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_history_window_with_bundle(self,
|
||||||
|
assets,
|
||||||
|
end_dt,
|
||||||
|
bar_count,
|
||||||
|
frequency,
|
||||||
|
field,
|
||||||
|
data_frequency=None,
|
||||||
|
ffill=True,
|
||||||
|
force_auto_ingest=False):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Public API method that returns a dataframe containing the requested
|
||||||
|
history window. Data is fully adjusted.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
assets : list[TradingPair]
|
||||||
|
The assets whose data is desired.
|
||||||
|
|
||||||
|
end_dt: datetime
|
||||||
|
The date of the last bar.
|
||||||
|
|
||||||
|
bar_count: int
|
||||||
|
The number of bars desired.
|
||||||
|
|
||||||
|
frequency: string
|
||||||
|
"1d" or "1m"
|
||||||
|
|
||||||
|
field: string
|
||||||
|
The desired field of the asset.
|
||||||
|
|
||||||
|
data_frequency: string
|
||||||
|
The frequency of the data to query; i.e. whether the data is
|
||||||
|
'daily' or 'minute' bars.
|
||||||
|
|
||||||
|
# TODO: fill how?
|
||||||
|
ffill: boolean
|
||||||
|
Forward-fill missing values. Only has effect if field
|
||||||
|
is 'price'.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
A dataframe containing the requested data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
freq, candle_size, unit, data_frequency = get_frequency(
|
||||||
|
frequency, data_frequency
|
||||||
|
)
|
||||||
|
adj_bar_count = candle_size * bar_count
|
||||||
|
try:
|
||||||
|
series = self.bundle.get_history_window_series_and_load(
|
||||||
|
assets=assets,
|
||||||
|
end_dt=end_dt,
|
||||||
|
bar_count=adj_bar_count,
|
||||||
|
field=field,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
force_auto_ingest=force_auto_ingest
|
||||||
|
)
|
||||||
|
except (PricingDataNotLoadedError, NoDataAvailableOnExchange):
|
||||||
|
series = dict()
|
||||||
|
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
asset_candles = candles[asset]
|
if asset not in series or series[asset].index[-1] < end_dt:
|
||||||
|
# Adding bars too recent to be contained in the consolidated
|
||||||
|
# exchanges bundles. We go directly against the exchange
|
||||||
|
# to retrieve the candles.
|
||||||
|
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
|
||||||
|
trailing_dt = \
|
||||||
|
series[asset].index[-1] + get_delta(1, data_frequency) \
|
||||||
|
if asset in series else start_dt
|
||||||
|
|
||||||
values = map(lambda candle: candle[field], asset_candles)
|
# The get_history method supports multiple asset
|
||||||
dates = map(lambda candle: candle['last_traded'], asset_candles)
|
# Use the original frequency to let each api optimize
|
||||||
|
# the size of result sets
|
||||||
|
trailing_bar_count = get_periods(
|
||||||
|
trailing_dt, end_dt, freq
|
||||||
|
)
|
||||||
|
candles = self.get_candles(
|
||||||
|
freq=freq,
|
||||||
|
assets=asset,
|
||||||
|
bar_count=trailing_bar_count,
|
||||||
|
start_dt=start_dt,
|
||||||
|
end_dt=end_dt
|
||||||
|
)
|
||||||
|
|
||||||
value_series = pd.Series(values, index=dates)
|
last_value = series[asset].iloc(0) if asset in series \
|
||||||
series[asset] = value_series
|
else np.nan
|
||||||
|
|
||||||
|
# Create a series with the common data_frequency, ffill
|
||||||
|
# missing values
|
||||||
|
candle_series = self.get_series_from_candles(
|
||||||
|
candles=candles,
|
||||||
|
start_dt=trailing_dt,
|
||||||
|
end_dt=end_dt,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
field=field,
|
||||||
|
previous_value=last_value
|
||||||
|
)
|
||||||
|
|
||||||
|
if asset in series:
|
||||||
|
series[asset].append(candle_series)
|
||||||
|
|
||||||
|
else:
|
||||||
|
series[asset] = candle_series
|
||||||
|
|
||||||
|
df = resample_history_df(pd.DataFrame(series), freq, field)
|
||||||
|
# TODO: consider this more carefully
|
||||||
|
df.dropna(inplace=True)
|
||||||
|
|
||||||
df = pd.concat(series)
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def synchronize_portfolio(self):
|
def synchronize_portfolio(self):
|
||||||
@@ -402,7 +687,6 @@ class Exchange:
|
|||||||
Update the portfolio cash and position balances based on the
|
Update the portfolio cash and position balances based on the
|
||||||
latest ticker prices.
|
latest ticker prices.
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
"""
|
||||||
log.debug('synchronizing portfolio with exchange {}'.format(self.name))
|
log.debug('synchronizing portfolio with exchange {}'.format(self.name))
|
||||||
balances = self.get_balances()
|
balances = self.get_balances()
|
||||||
@@ -413,7 +697,7 @@ class Exchange:
|
|||||||
if base_position_available is None:
|
if base_position_available is None:
|
||||||
raise BaseCurrencyNotFoundError(
|
raise BaseCurrencyNotFoundError(
|
||||||
base_currency=self.base_currency,
|
base_currency=self.base_currency,
|
||||||
exchange=self.name
|
exchange=self.name.title()
|
||||||
)
|
)
|
||||||
|
|
||||||
portfolio = self._portfolio
|
portfolio = self._portfolio
|
||||||
@@ -424,7 +708,7 @@ class Exchange:
|
|||||||
portfolio.starting_cash = portfolio.cash
|
portfolio.starting_cash = portfolio.cash
|
||||||
|
|
||||||
if portfolio.positions:
|
if portfolio.positions:
|
||||||
assets = portfolio.positions.keys()
|
assets = list(portfolio.positions.keys())
|
||||||
tickers = self.tickers(assets)
|
tickers = self.tickers(assets)
|
||||||
|
|
||||||
portfolio.positions_value = 0.0
|
portfolio.positions_value = 0.0
|
||||||
@@ -440,34 +724,26 @@ class Exchange:
|
|||||||
portfolio.portfolio_value = \
|
portfolio.portfolio_value = \
|
||||||
portfolio.positions_value + portfolio.cash
|
portfolio.positions_value + portfolio.cash
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_balances(self):
|
|
||||||
"""
|
|
||||||
Retrieve wallet balances for the exchange
|
|
||||||
:return balances: A dict of currency => available balance
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def create_order(self, asset, amount, is_buy, style):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def order(self, asset, amount, limit_price=None, stop_price=None,
|
def order(self, asset, amount, limit_price=None, stop_price=None,
|
||||||
style=None):
|
style=None):
|
||||||
"""Place an order.
|
"""Place an order.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
asset : Asset
|
asset : TradingPair
|
||||||
The asset that this order is for.
|
The asset that this order is for.
|
||||||
|
|
||||||
amount : int
|
amount : int
|
||||||
The amount of shares to order. If ``amount`` is positive, this is
|
The amount of shares to order. If ``amount`` is positive, this is
|
||||||
the number of shares to buy or cover. If ``amount`` is negative,
|
the number of shares to buy or cover. If ``amount`` is negative,
|
||||||
this is the number of shares to sell or short.
|
this is the number of shares to sell or short.
|
||||||
|
|
||||||
limit_price : float, optional
|
limit_price : float, optional
|
||||||
The limit price for the order.
|
The limit price for the order.
|
||||||
|
|
||||||
stop_price : float, optional
|
stop_price : float, optional
|
||||||
The stop price for the order.
|
The stop price for the order.
|
||||||
|
|
||||||
style : ExecutionStyle, optional
|
style : ExecutionStyle, optional
|
||||||
The execution style for the order.
|
The execution style for the order.
|
||||||
|
|
||||||
@@ -492,6 +768,7 @@ class Exchange:
|
|||||||
:class:`catalyst.finance.execution.ExecutionStyle`
|
:class:`catalyst.finance.execution.ExecutionStyle`
|
||||||
:func:`catalyst.api.order_value`
|
:func:`catalyst.api.order_value`
|
||||||
:func:`catalyst.api.order_percent`
|
:func:`catalyst.api.order_percent`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if amount == 0:
|
if amount == 0:
|
||||||
log.warn('skipping order amount of 0')
|
log.warn('skipping order amount of 0')
|
||||||
@@ -515,7 +792,7 @@ class Exchange:
|
|||||||
style = ExchangeStopOrder(stop_price, exchange=self.name)
|
style = ExchangeStopOrder(stop_price, exchange=self.name)
|
||||||
|
|
||||||
elif style is not None:
|
elif style is not None:
|
||||||
raise InvalidOrderStyle(exchange=self.name,
|
raise InvalidOrderStyle(exchange=self.name.title(),
|
||||||
style=style.__class__.__name__)
|
style=style.__class__.__name__)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Incomplete order data.')
|
raise ValueError('Incomplete order data.')
|
||||||
@@ -531,10 +808,51 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
order = self.create_order(asset, amount, is_buy, style)
|
order = self.create_order(asset, amount, is_buy, style)
|
||||||
|
if order:
|
||||||
self._portfolio.create_order(order)
|
self._portfolio.create_order(order)
|
||||||
|
|
||||||
return order.id
|
return order.id
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# The methods below must be implemented for each exchange.
|
||||||
|
@abstractmethod
|
||||||
|
def get_balances(self):
|
||||||
|
"""
|
||||||
|
Retrieve wallet balances for the exchange.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict[TradingPair, float]
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_order(self, asset, amount, is_buy, style):
|
||||||
|
"""
|
||||||
|
Place an order on the exchange.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset: TradingPair
|
||||||
|
The target market.
|
||||||
|
|
||||||
|
amount: float
|
||||||
|
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.
|
||||||
|
|
||||||
|
is_buy: bool
|
||||||
|
Is it a buy order?
|
||||||
|
|
||||||
|
style: ExecutionStyle
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Order
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_open_orders(self, asset):
|
def get_open_orders(self, asset):
|
||||||
@@ -587,16 +905,43 @@ class Exchange:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_candles(self, data_frequency, assets, bar_count=None):
|
def get_candles(self, freq, assets, bar_count=None,
|
||||||
|
start_dt=None, end_dt=None):
|
||||||
"""
|
"""
|
||||||
Retrieve OHLCV candles for the given assets
|
Retrieve OHLCV candles for the given assets
|
||||||
|
|
||||||
:param data_frequency:
|
Parameters
|
||||||
:param assets:
|
----------
|
||||||
:param end_dt:
|
freq: str
|
||||||
:param bar_count:
|
The frequency alias per convention:
|
||||||
:param limit:
|
http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
||||||
:return:
|
|
||||||
|
assets: list[TradingPair]
|
||||||
|
The targeted assets.
|
||||||
|
|
||||||
|
bar_count: int
|
||||||
|
The number of bar desired. (default 1)
|
||||||
|
|
||||||
|
end_dt: datetime, optional
|
||||||
|
The last bar date.
|
||||||
|
|
||||||
|
start_dt: datetime, optional
|
||||||
|
The first bar date.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict[TradingPair, dict[str, Object]]
|
||||||
|
A dictionary of OHLCV candles. Each TradingPair instance is
|
||||||
|
mapped to a list of dictionaries with this structure:
|
||||||
|
open: float
|
||||||
|
high: float
|
||||||
|
low: float
|
||||||
|
close: float
|
||||||
|
volume: float
|
||||||
|
last_traded: datetime
|
||||||
|
|
||||||
|
See definition here:
|
||||||
|
http://www.investopedia.com/terms/o/ohlcchart.asp
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -605,8 +950,14 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Retrieve current tick data for the given assets
|
Retrieve current tick data for the given assets
|
||||||
|
|
||||||
:param assets:
|
Parameters
|
||||||
:return:
|
----------
|
||||||
|
assets: list[TradingPair]
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[dict[str, float]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -614,6 +965,23 @@ class Exchange:
|
|||||||
def get_account(self):
|
def get_account(self):
|
||||||
"""
|
"""
|
||||||
Retrieve the account parameters.
|
Retrieve the account parameters.
|
||||||
:return:
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_orderbook(self, asset, order_type, limit):
|
||||||
|
"""
|
||||||
|
Retrieve the the orderbook for the given trading pair.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
asset: TradingPair
|
||||||
|
order_type: str
|
||||||
|
The type of orders: bid, ask or all
|
||||||
|
limit: int
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list[dict[str, float]
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,829 @@
|
|||||||
|
#
|
||||||
|
# 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._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)
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
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
@@ -0,0 +1,406 @@
|
|||||||
|
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,6 +1,24 @@
|
|||||||
|
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}'
|
||||||
@@ -34,6 +52,13 @@ 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 '
|
||||||
@@ -41,6 +66,13 @@ class ExchangeAuthNotFound(ZiplineError):
|
|||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeAuthEmpty(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Please enter your API token key and secret for exchange {exchange} '
|
||||||
|
'in the following file: {filename}'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class ExchangeSymbolsNotFound(ZiplineError):
|
class ExchangeSymbolsNotFound(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Unable to download or find a local copy of symbols.json for exchange '
|
'Unable to download or find a local copy of symbols.json for exchange '
|
||||||
@@ -54,9 +86,24 @@ 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 = (
|
||||||
'History frequency {frequency} not supported by the exchange.'
|
'Frequency {frequency} not supported by the exchange.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class MismatchingFrequencyError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Bar aggregate frequency {frequency} not compatible with '
|
||||||
|
'data frequency {data_frequency}.'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -87,6 +134,19 @@ class OrderNotFound(ZiplineError):
|
|||||||
).strip()
|
).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):
|
class OrderCancelError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'Unable to cancel order {order_id} on exchange {exchange} {error}.'
|
'Unable to cancel order {order_id} on exchange {exchange} {error}.'
|
||||||
@@ -111,3 +171,72 @@ class MismatchingBaseCurrencies(ZiplineError):
|
|||||||
'Unable to trade with base currency {base_currency} when the '
|
'Unable to trade with base currency {base_currency} when the '
|
||||||
'algorithm uses {algo_currency}.'
|
'algorithm uses {algo_currency}.'
|
||||||
).strip()
|
).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()
|
||||||
|
|||||||
@@ -4,9 +4,16 @@ from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder
|
|||||||
class ExchangeLimitOrder(LimitOrder):
|
class ExchangeLimitOrder(LimitOrder):
|
||||||
def get_limit_price(self, is_buy):
|
def get_limit_price(self, is_buy):
|
||||||
"""
|
"""
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
||||||
:param is_buy:
|
|
||||||
:return:
|
Parameters
|
||||||
|
----------
|
||||||
|
is_buy: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.limit_price
|
return self.limit_price
|
||||||
|
|
||||||
@@ -14,9 +21,16 @@ class ExchangeLimitOrder(LimitOrder):
|
|||||||
class ExchangeStopOrder(StopOrder):
|
class ExchangeStopOrder(StopOrder):
|
||||||
def get_stop_price(self, is_buy):
|
def get_stop_price(self, is_buy):
|
||||||
"""
|
"""
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
||||||
:param is_buy:
|
|
||||||
:return:
|
Parameters
|
||||||
|
----------
|
||||||
|
is_buy: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.stop_price
|
return self.stop_price
|
||||||
|
|
||||||
@@ -24,16 +38,30 @@ class ExchangeStopOrder(StopOrder):
|
|||||||
class ExchangeStopLimitOrder(StopLimitOrder):
|
class ExchangeStopLimitOrder(StopLimitOrder):
|
||||||
def get_limit_price(self, is_buy):
|
def get_limit_price(self, is_buy):
|
||||||
"""
|
"""
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
||||||
:param is_buy:
|
|
||||||
:return:
|
Parameters
|
||||||
|
----------
|
||||||
|
is_buy: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.limit_price
|
return self.limit_price
|
||||||
|
|
||||||
def get_stop_price(self, is_buy):
|
def get_stop_price(self, is_buy):
|
||||||
"""
|
"""
|
||||||
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
We may be trading Satoshis with 8 decimals, we cannot round numbers.
|
||||||
:param is_buy:
|
|
||||||
:return:
|
Parameters
|
||||||
|
----------
|
||||||
|
is_buy: bool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.stop_price
|
return self.stop_price
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
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
|
||||||
|
from catalyst.utils.deprecate import deprecated
|
||||||
|
|
||||||
log = Logger('ExchangePortfolio')
|
log = Logger('ExchangePortfolio', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class ExchangePortfolio(Portfolio):
|
class ExchangePortfolio(Portfolio):
|
||||||
@@ -28,10 +30,15 @@ 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
|
||||||
|
|
||||||
@@ -46,6 +53,18 @@ 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]
|
||||||
|
|
||||||
@@ -70,7 +89,41 @@ class ExchangePortfolio(Portfolio):
|
|||||||
|
|
||||||
log.debug('updated portfolio with executed order')
|
log.debug('updated portfolio with executed order')
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def execute_transaction(self, transaction):
|
||||||
|
# TODO: almost duplicate of execute_order. Not sure why Poloniex needs this.
|
||||||
|
log.debug('executing transaction {}'.format(transaction.order_id))
|
||||||
|
|
||||||
|
order_position = self.positions[transaction.asset] \
|
||||||
|
if transaction.asset in self.positions else None
|
||||||
|
|
||||||
|
if order_position is None:
|
||||||
|
raise ValueError(
|
||||||
|
'Trying to execute transaction for a position not held: %s' % transaction.order_id
|
||||||
|
)
|
||||||
|
|
||||||
|
self.capital_used += transaction.amount * transaction.price
|
||||||
|
|
||||||
|
if transaction.amount > 0:
|
||||||
|
if order_position.cost_basis > 0:
|
||||||
|
order_position.cost_basis = np.average(
|
||||||
|
[order_position.cost_basis, transaction.price],
|
||||||
|
weights=[order_position.amount, transaction.amount]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
order_position.cost_basis = transaction.price
|
||||||
|
|
||||||
|
log.debug('updated portfolio with executed order')
|
||||||
|
|
||||||
def remove_order(self, order):
|
def remove_order(self, order):
|
||||||
|
"""
|
||||||
|
Removing an open order.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order: Order
|
||||||
|
|
||||||
|
"""
|
||||||
log.info('removing cancelled order {}'.format(order.id))
|
log.info('removing cancelled order {}'.format(order.id))
|
||||||
del self.open_orders[order.id]
|
del self.open_orders[order.id]
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,56 @@
|
|||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import urllib
|
import re
|
||||||
|
import shutil
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
from catalyst.exchange.exchange_errors import ExchangeAuthNotFound, \
|
import pandas as pd
|
||||||
ExchangeSymbolsNotFound
|
from catalyst.assets._assets import TradingPair
|
||||||
from catalyst.utils.paths import data_root, ensure_directory
|
from six.moves.urllib import request
|
||||||
|
|
||||||
# TODO: move to aws
|
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
|
||||||
SYMBOLS_URL = 'https://raw.githubusercontent.com/enigmampc/catalyst/' \
|
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \
|
||||||
'master/catalyst/exchange/{exchange}/symbols.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
|
||||||
|
|
||||||
@@ -24,26 +61,75 @@ def get_exchange_folder(exchange_name, environ=None):
|
|||||||
return exchange_folder
|
return exchange_folder
|
||||||
|
|
||||||
|
|
||||||
def download_exchange_symbols(exchange_name, environ=None):
|
def get_exchange_symbols_filename(exchange_name, is_local=False, environ=None):
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
"""
|
||||||
filename = os.path.join(exchange_folder, 'symbols.json')
|
The absolute path of the exchange's symbol.json file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name:
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = 'symbols.json' if not is_local else 'symbols_local.json'
|
||||||
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
|
return os.path.join(exchange_folder, name)
|
||||||
|
|
||||||
|
|
||||||
|
def download_exchange_symbols(exchange_name, environ=None):
|
||||||
|
"""
|
||||||
|
Downloads the exchange's symbols.json from the repository.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
exchange_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
filename = get_exchange_symbols_filename(exchange_name)
|
||||||
url = SYMBOLS_URL.format(exchange=exchange_name)
|
url = SYMBOLS_URL.format(exchange=exchange_name)
|
||||||
response = urllib.urlretrieve(url=url, filename=filename)
|
response = request.urlretrieve(url=url, filename=filename)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_symbols(exchange_name, environ=None):
|
def get_exchange_symbols(exchange_name, is_local=False, environ=None):
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
"""
|
||||||
filename = os.path.join(exchange_folder, 'symbols.json')
|
The de-serialized content of the exchange's symbols.json.
|
||||||
|
|
||||||
if not os.path.isfile(filename):
|
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)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
except ValueError:
|
||||||
|
return dict()
|
||||||
else:
|
else:
|
||||||
raise ExchangeSymbolsNotFound(
|
raise ExchangeSymbolsNotFound(
|
||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
@@ -51,7 +137,63 @@ def get_exchange_symbols(exchange_name, 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')
|
||||||
|
|
||||||
@@ -60,13 +202,45 @@ def get_exchange_auth(exchange_name, environ=None):
|
|||||||
data = json.load(data_file)
|
data = json.load(data_file)
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
raise ExchangeAuthNotFound(
|
data = dict(name=exchange_name, key='', secret='')
|
||||||
exchange=exchange_name,
|
with open(filename, 'w') as f:
|
||||||
filename=filename
|
json.dump(data, f, sort_keys=False, indent=2,
|
||||||
)
|
separators=(',', ':'))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def delete_algo_folder(algo_name, environ=None):
|
||||||
|
"""
|
||||||
|
Delete the folder containing the algo state.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
|
||||||
|
|
||||||
def get_algo_folder(algo_name, environ=None):
|
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
|
||||||
|
|
||||||
@@ -78,6 +252,24 @@ 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:
|
||||||
@@ -96,6 +288,18 @@ 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:
|
||||||
@@ -108,16 +312,77 @@ 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 append_algo_object(algo_name, key, obj, environ=None):
|
def get_algo_df(algo_name, key, environ=None, rel_path=None):
|
||||||
algo_folder = get_algo_folder(algo_name, environ)
|
"""
|
||||||
filename = os.path.join(algo_folder, key + '.p')
|
The de-serialized DataFrame of an algo name and key.
|
||||||
|
|
||||||
mode = 'a+b' if os.path.isfile(filename) else 'wb'
|
Parameters
|
||||||
with open(filename, mode) as handle:
|
----------
|
||||||
pickle.dump(obj, handle, protocol=pickle.HIGHEST_PROTOCOL)
|
algo_name: str
|
||||||
|
key: str
|
||||||
|
environ:
|
||||||
|
rel_path: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DataFrame
|
||||||
|
|
||||||
|
"""
|
||||||
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
|
||||||
|
if rel_path is not None:
|
||||||
|
folder = os.path.join(folder, rel_path)
|
||||||
|
|
||||||
|
filename = os.path.join(folder, key + '.csv')
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
try:
|
||||||
|
with open(filename, 'rb') as handle:
|
||||||
|
return pd.read_csv(handle, index_col=0, parse_dates=True)
|
||||||
|
except IOError:
|
||||||
|
return pd.DataFrame()
|
||||||
|
else:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
|
||||||
|
def save_algo_df(algo_name, key, df, environ=None, rel_path=None):
|
||||||
|
"""
|
||||||
|
Serialize to csv and save a DataFrame by algo name and key.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
algo_name: str
|
||||||
|
key: str
|
||||||
|
df: pd.DataFrame
|
||||||
|
environ:
|
||||||
|
rel_path: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
folder = get_algo_folder(algo_name, environ)
|
||||||
|
if rel_path is not None:
|
||||||
|
folder = os.path.join(folder, rel_path)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
filename = os.path.join(folder, key + '.csv')
|
||||||
|
|
||||||
|
with open(filename, 'wt') as handle:
|
||||||
|
df.to_csv(handle, encoding='UTF_8')
|
||||||
|
|
||||||
|
|
||||||
def 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')
|
||||||
@@ -126,9 +391,183 @@ def get_exchange_minute_writer_root(exchange_name, environ=None):
|
|||||||
return minute_data_folder
|
return minute_data_folder
|
||||||
|
|
||||||
|
|
||||||
def perf_serial(obj):
|
def get_exchange_bundles_folder(exchange_name, environ=None):
|
||||||
"""JSON serializer for objects not serializable by default json code"""
|
"""
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
|
||||||
|
from catalyst.exchange.bittrex.bittrex import Bittrex
|
||||||
|
from catalyst.exchange.exchange_errors import ExchangeNotFoundError
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_auth
|
||||||
|
from catalyst.exchange.poloniex.poloniex import Poloniex
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchange(exchange_name, base_currency=None):
|
||||||
|
exchange_auth = get_exchange_auth(exchange_name)
|
||||||
|
if exchange_name == 'bitfinex':
|
||||||
|
return Bitfinex(
|
||||||
|
key=exchange_auth['key'],
|
||||||
|
secret=exchange_auth['secret'],
|
||||||
|
base_currency=base_currency,
|
||||||
|
portfolio=None
|
||||||
|
)
|
||||||
|
|
||||||
|
elif exchange_name == 'bittrex':
|
||||||
|
return Bittrex(
|
||||||
|
key=exchange_auth['key'],
|
||||||
|
secret=exchange_auth['secret'],
|
||||||
|
base_currency=base_currency,
|
||||||
|
portfolio=None
|
||||||
|
)
|
||||||
|
|
||||||
|
elif exchange_name == 'poloniex':
|
||||||
|
return Poloniex(
|
||||||
|
key=exchange_auth['key'],
|
||||||
|
secret=exchange_auth['secret'],
|
||||||
|
base_currency=base_currency,
|
||||||
|
portfolio=None
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_exchanges(exchange_names):
|
||||||
|
exchanges = dict()
|
||||||
|
for exchange_name in exchange_names:
|
||||||
|
exchanges[exchange_name] = get_exchange(exchange_name)
|
||||||
|
|
||||||
|
return exchanges
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
#!/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')
|
||||||
@@ -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
|
||||||
|
|
||||||
log = Logger('ExchangeClock')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('ExchangeClock', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class ExchangeClock(object):
|
class SimpleClock(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,14 +1,138 @@
|
|||||||
|
import numbers
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
def get_pretty_stats(stats_df, num_rows=10):
|
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.
|
Format and print the last few rows of a statistics DataFrame.
|
||||||
See the pyfolio project for the data structure.
|
See the pyfolio project for the data structure.
|
||||||
|
|
||||||
:param stats_df:
|
Parameters
|
||||||
:param num_rows:
|
----------
|
||||||
:return:
|
stats_df: DataFrame
|
||||||
|
num_rows: int
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
stats_df.set_index('period_close', drop=True, inplace=True)
|
stats_df.set_index('period_close', drop=True, inplace=True)
|
||||||
stats_df.dropna(axis=1, how='all', inplace=True)
|
stats_df.dropna(axis=1, how='all', inplace=True)
|
||||||
@@ -22,6 +146,10 @@ def get_pretty_stats(stats_df, num_rows=10):
|
|||||||
'pnl', 'long_exposure', 'short_exposure', 'orders',
|
'pnl', 'long_exposure', 'short_exposure', 'orders',
|
||||||
'transactions', 'positions']
|
'transactions', 'positions']
|
||||||
|
|
||||||
|
if recorded_cols is not None:
|
||||||
|
for column in recorded_cols:
|
||||||
|
columns.append(column)
|
||||||
|
|
||||||
def format_positions(positions):
|
def format_positions(positions):
|
||||||
parts = []
|
parts = []
|
||||||
for position in positions:
|
for position in positions:
|
||||||
@@ -45,3 +173,49 @@ def get_pretty_stats(stats_df, num_rows=10):
|
|||||||
columns=columns,
|
columns=columns,
|
||||||
formatters=formatters
|
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
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
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,7 +34,9 @@ from catalyst.finance.commission import (
|
|||||||
from catalyst.finance.cancel_policy import NeverCancel
|
from catalyst.finance.cancel_policy import NeverCancel
|
||||||
from catalyst.utils.input_validation import expect_types
|
from catalyst.utils.input_validation import expect_types
|
||||||
|
|
||||||
log = Logger('Blotter')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('Blotter', level=LOG_LEVEL)
|
||||||
warning_logger = Logger('AlgoWarning')
|
warning_logger = Logger('AlgoWarning')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ from catalyst.errors import (
|
|||||||
TradingControlViolation,
|
TradingControlViolation,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = logbook.Logger('TradingControl')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('TradingControl', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class TradingControl(with_metaclass(abc.ABCMeta)):
|
class TradingControl(with_metaclass(abc.ABCMeta)):
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ class LimitOrder(ExecutionStyle):
|
|||||||
Execution style representing an order to be executed at a price equal to or
|
Execution style representing an order to be executed at a price equal to or
|
||||||
better than a specified limit price.
|
better than a specified limit price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, limit_price, exchange=None):
|
def __init__(self, limit_price, exchange=None):
|
||||||
"""
|
"""
|
||||||
Store the given price.
|
Store the given price.
|
||||||
@@ -99,6 +100,7 @@ class StopOrder(ExecutionStyle):
|
|||||||
Execution style representing an order to be placed once the market price
|
Execution style representing an order to be placed once the market price
|
||||||
reaches a specified stop price.
|
reaches a specified stop price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, stop_price, exchange=None):
|
def __init__(self, stop_price, exchange=None):
|
||||||
"""
|
"""
|
||||||
Store the given price.
|
Store the given price.
|
||||||
@@ -121,6 +123,7 @@ class StopLimitOrder(ExecutionStyle):
|
|||||||
Execution style representing a limit order to be placed with a specified
|
Execution style representing a limit order to be placed with a specified
|
||||||
limit price once the market reaches a specified stop price.
|
limit price once the market reaches a specified stop price.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, limit_price, stop_price, exchange=None):
|
def __init__(self, limit_price, stop_price, exchange=None):
|
||||||
"""
|
"""
|
||||||
Store the given prices
|
Store the given prices
|
||||||
@@ -144,31 +147,20 @@ class StopLimitOrder(ExecutionStyle):
|
|||||||
def asymmetric_round_price_to_penny(price, prefer_round_down,
|
def asymmetric_round_price_to_penny(price, prefer_round_down,
|
||||||
diff=(0.0095 - .005)):
|
diff=(0.0095 - .005)):
|
||||||
"""
|
"""
|
||||||
Asymmetric rounding function for adjusting prices to two places in a way
|
Modified the original function because we do not want to round
|
||||||
that "improves" the price. For limit prices, this means preferring to
|
prices on crypto exchange.
|
||||||
round down on buys and preferring to round up on sells. For stop prices,
|
|
||||||
it means the reverse.
|
|
||||||
|
|
||||||
If prefer_round_down == True:
|
Parameters
|
||||||
When .05 below to .95 above a penny, use that penny.
|
----------
|
||||||
If prefer_round_down == False:
|
price: float
|
||||||
When .95 below to .05 above a penny, use that penny.
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
In math-speak:
|
|
||||||
If prefer_round_down: [<X-1>.0095, X.0195) -> round to X.01.
|
|
||||||
If not prefer_round_down: (<X-1>.0005, X.0105] -> round to X.01.
|
|
||||||
"""
|
"""
|
||||||
# Subtracting an epsilon from diff to enforce the open-ness of the upper
|
# TODO: consider overriding outside of the original function
|
||||||
# bound on buys and the lower bound on sells. Using the actual system
|
return price
|
||||||
# epsilon doesn't quite get there, so use a slightly less epsilon-ey value.
|
|
||||||
epsilon = float_info.epsilon * 10
|
|
||||||
diff = diff - epsilon
|
|
||||||
|
|
||||||
# relies on rounding half away from zero, unlike numpy's bankers' rounding
|
|
||||||
rounded = round(price - (diff if prefer_round_down else -diff), 2)
|
|
||||||
if zp_math.tolerant_equals(rounded, 0.0):
|
|
||||||
return 0.0
|
|
||||||
return rounded
|
|
||||||
|
|
||||||
|
|
||||||
def check_stoplimit_prices(price, label):
|
def check_stoplimit_prices(price, label):
|
||||||
|
|||||||
@@ -88,7 +88,10 @@ from six import itervalues, iteritems
|
|||||||
|
|
||||||
import catalyst.protocol as zp
|
import catalyst.protocol as zp
|
||||||
|
|
||||||
log = logbook.Logger('Performance')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
||||||
|
|
||||||
TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE
|
TRADE_TYPE = zp.DATASOURCE_TYPE.TRADE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ import logbook
|
|||||||
from catalyst.assets import Future, Asset
|
from catalyst.assets import Future, Asset
|
||||||
from catalyst.utils.input_validation import expect_types
|
from catalyst.utils.input_validation import expect_types
|
||||||
|
|
||||||
log = logbook.Logger('Performance')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class Position(object):
|
class Position(object):
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ from catalyst.assets import (
|
|||||||
)
|
)
|
||||||
from . position import positiondict
|
from . position import positiondict
|
||||||
|
|
||||||
log = logbook.Logger('Performance')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
PositionStats = namedtuple('PositionStats',
|
PositionStats = namedtuple('PositionStats',
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ import catalyst.finance.risk as risk
|
|||||||
|
|
||||||
from . position_tracker import PositionTracker
|
from . position_tracker import PositionTracker
|
||||||
|
|
||||||
log = logbook.Logger('Performance')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Performance', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class PerformanceTracker(object):
|
class PerformanceTracker(object):
|
||||||
@@ -111,27 +113,11 @@ 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,
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ from empyrical import (
|
|||||||
sharpe_ratio,
|
sharpe_ratio,
|
||||||
sortino_ratio,
|
sortino_ratio,
|
||||||
)
|
)
|
||||||
|
import warnings
|
||||||
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
log = logbook.Logger('Risk Cumulative')
|
log = logbook.Logger('Risk Cumulative', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
choose_treasury = functools.partial(choose_treasury, lambda *args: '10year',
|
choose_treasury = functools.partial(choose_treasury, lambda *args: '10year',
|
||||||
compound=False)
|
compound=False)
|
||||||
@@ -143,6 +144,8 @@ class RiskMetricsCumulative(object):
|
|||||||
self.num_trading_days = 0
|
self.num_trading_days = 0
|
||||||
|
|
||||||
def update(self, dt, algorithm_returns, benchmark_returns, leverage):
|
def update(self, dt, algorithm_returns, benchmark_returns, leverage):
|
||||||
|
warnings.filterwarnings('error')
|
||||||
|
|
||||||
# Keep track of latest dt for use in to_dict and other methods
|
# Keep track of latest dt for use in to_dict and other methods
|
||||||
# that report current state.
|
# that report current state.
|
||||||
self.latest_dt = dt
|
self.latest_dt = dt
|
||||||
@@ -189,9 +192,12 @@ 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]
|
||||||
@@ -266,10 +272,17 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
risk = self.downside_risk[dt_loc]
|
||||||
self.sortino[dt_loc] = sortino_ratio(
|
self.sortino[dt_loc] = sortino_ratio(
|
||||||
self.algorithm_returns,
|
self.algorithm_returns,
|
||||||
_downside_risk=self.downside_risk[dt_loc]
|
_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,
|
||||||
@@ -281,6 +294,8 @@ 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.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import warnings
|
||||||
|
|
||||||
import logbook
|
import logbook
|
||||||
|
|
||||||
@@ -36,7 +37,9 @@ from empyrical import (
|
|||||||
sortino_ratio
|
sortino_ratio
|
||||||
)
|
)
|
||||||
|
|
||||||
log = logbook.Logger('Risk Period')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Risk Period', level=LOG_LEVEL)
|
||||||
|
|
||||||
choose_treasury = functools.partial(risk.choose_treasury,
|
choose_treasury = functools.partial(risk.choose_treasury,
|
||||||
risk.select_treasury_duration)
|
risk.select_treasury_duration)
|
||||||
@@ -76,8 +79,14 @@ class RiskMetricsPeriod(object):
|
|||||||
self.calculate_metrics()
|
self.calculate_metrics()
|
||||||
|
|
||||||
def calculate_metrics(self):
|
def calculate_metrics(self):
|
||||||
|
warnings.filterwarnings('error')
|
||||||
|
|
||||||
|
try:
|
||||||
self.benchmark_period_returns = \
|
self.benchmark_period_returns = \
|
||||||
cum_returns(self.benchmark_returns).iloc[-1]
|
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]
|
||||||
@@ -126,10 +135,17 @@ class RiskMetricsPeriod(object):
|
|||||||
self.downside_risk = downside_risk(
|
self.downside_risk = downside_risk(
|
||||||
self.algorithm_returns.values
|
self.algorithm_returns.values
|
||||||
)
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
risk = self.downside_risk
|
||||||
self.sortino = sortino_ratio(
|
self.sortino = sortino_ratio(
|
||||||
self.algorithm_returns.values,
|
self.algorithm_returns.values,
|
||||||
_downside_risk=self.downside_risk,
|
_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,
|
||||||
@@ -143,6 +159,8 @@ class RiskMetricsPeriod(object):
|
|||||||
self.max_drawdown = max_drawdown(self.algorithm_returns.values)
|
self.max_drawdown = max_drawdown(self.algorithm_returns.values)
|
||||||
self.max_leverage = self.calculate_max_leverage()
|
self.max_leverage = self.calculate_max_leverage()
|
||||||
|
|
||||||
|
warnings.resetwarnings()
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
Creates a dictionary representing the state of the risk report.
|
Creates a dictionary representing the state of the risk report.
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ from dateutil.relativedelta import relativedelta
|
|||||||
|
|
||||||
from . period import RiskMetricsPeriod
|
from . period import RiskMetricsPeriod
|
||||||
|
|
||||||
log = logbook.Logger('Risk Report')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Risk Report', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class RiskReport(object):
|
class RiskReport(object):
|
||||||
|
|||||||
@@ -61,7 +61,9 @@ Risk Report
|
|||||||
import logbook
|
import logbook
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
log = logbook.Logger('Risk')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Risk', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
TREASURY_DURATIONS = [
|
TREASURY_DURATIONS = [
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ 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
|
||||||
|
|
||||||
@@ -205,20 +206,22 @@ 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 < 1:
|
if remaining_volume < min_trade_size:
|
||||||
# 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 = int(min(remaining_volume, abs(order.open_amount)))
|
cur_volume = min(remaining_volume, abs(order.open_amount))
|
||||||
|
|
||||||
if cur_volume < 1:
|
if cur_volume < min_trade_size:
|
||||||
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,7 +26,9 @@ from catalyst.data.loader import load_market_data
|
|||||||
from catalyst.utils.calendars import get_calendar
|
from catalyst.utils.calendars import get_calendar
|
||||||
from catalyst.utils.memoize import remember_last
|
from catalyst.utils.memoize import remember_last
|
||||||
|
|
||||||
log = logbook.Logger('Trading')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = logbook.Logger('Trading', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CAPITAL_BASE = 1e5
|
DEFAULT_CAPITAL_BASE = 1e5
|
||||||
|
|||||||
@@ -65,14 +65,10 @@ 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=int(amount),
|
amount=amount,
|
||||||
dt=dt,
|
dt=dt,
|
||||||
price=price,
|
price=price,
|
||||||
order_id=order.id
|
order_id=order.id
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ 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
|
||||||
@@ -117,24 +115,3 @@ 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,14 +27,15 @@ from catalyst.gens.sim_engine import (
|
|||||||
BEFORE_TRADING_START_BAR
|
BEFORE_TRADING_START_BAR
|
||||||
)
|
)
|
||||||
|
|
||||||
log = Logger('Trade Simulation')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('Trade Simulation', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class AlgorithmSimulator(object):
|
class AlgorithmSimulator(object):
|
||||||
|
|
||||||
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'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +203,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 in set(('minute', '5-minute')):
|
if algo.data_frequency == '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,10 +41,6 @@ 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,8 +40,6 @@ 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:
|
||||||
@@ -53,9 +51,6 @@ 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,19 +65,6 @@ 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:
|
||||||
@@ -85,7 +72,13 @@ class BenchmarkSource(object):
|
|||||||
"benchmark_returns.")
|
"benchmark_returns.")
|
||||||
|
|
||||||
def get_value(self, dt):
|
def get_value(self, dt):
|
||||||
return self._precalculated_series.loc[dt]
|
try:
|
||||||
|
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]
|
||||||
@@ -168,21 +161,6 @@ 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,7 +23,9 @@ from catalyst.protocol import (
|
|||||||
)
|
)
|
||||||
from catalyst.assets import Equity
|
from catalyst.assets import Equity
|
||||||
|
|
||||||
logger = Logger('Requests Source Logger')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
logger = Logger('Requests Source Logger', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
def roll_dts_to_midnight(dts, trading_day):
|
def roll_dts_to_midnight(dts, trading_day):
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
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')
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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'
|
||||||
|
)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
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, )
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
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'
|
||||||
|
)
|
||||||
@@ -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-03-01', tz='UTC'), **kwargs)
|
super(OpenExchangeCalendar, self).__init__(start=Timestamp('2015-3-1', tz='UTC'), **kwargs)
|
||||||
|
|||||||
@@ -118,9 +118,6 @@ 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]
|
||||||
|
|
||||||
@@ -182,18 +179,6 @@ 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):
|
||||||
"""
|
"""
|
||||||
@@ -386,10 +371,6 @@ 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.
|
||||||
@@ -484,12 +465,6 @@ 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
|
||||||
@@ -591,20 +566,6 @@ 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
|
||||||
@@ -662,15 +623,6 @@ 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
|
||||||
@@ -777,13 +729,6 @@ 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,7 +602,6 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import math
|
|||||||
|
|
||||||
from numpy import isnan
|
from numpy import isnan
|
||||||
|
|
||||||
|
def round_nearest(x, a):
|
||||||
|
return round(round(x / a) * a, -int(math.floor(math.log10(a))))
|
||||||
|
|
||||||
def tolerant_equals(a, b, atol=10e-7, rtol=10e-7, equal_nan=False):
|
def tolerant_equals(a, b, atol=10e-7, rtol=10e-7, equal_nan=False):
|
||||||
"""Check if a and b are equal with some tolerance.
|
"""Check if a and b are equal with some tolerance.
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ def catalyst_root(environ=None):
|
|||||||
|
|
||||||
root = environ.get('ZIPLINE_ROOT', None)
|
root = environ.get('ZIPLINE_ROOT', None)
|
||||||
if root is None:
|
if root is None:
|
||||||
root = expanduser('~/.catalyst')
|
root = os.path.join(expanduser('~'),'.catalyst')
|
||||||
|
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|||||||
+160
-121
@@ -1,16 +1,19 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from runpy import run_path
|
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from time import sleep
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from runpy import run_path
|
||||||
import pandas as pd
|
from time import sleep
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst.data.bundles import load
|
||||||
|
from catalyst.data.data_portal import DataPortal
|
||||||
from catalyst.exchange.bittrex.bittrex import Bittrex
|
from catalyst.exchange.bittrex.bittrex import Bittrex
|
||||||
|
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
|
||||||
|
from catalyst.exchange.poloniex.poloniex import Poloniex
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
@@ -23,34 +26,29 @@ except:
|
|||||||
from toolz import valfilter, concatv
|
from toolz import valfilter, concatv
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from catalyst.algorithm import TradingAlgorithm
|
|
||||||
from catalyst.data.bundles.core import load
|
|
||||||
from catalyst.data.data_portal import DataPortal
|
|
||||||
from catalyst.data.loader import load_crypto_market_data
|
|
||||||
from catalyst.finance.trading import TradingEnvironment
|
from catalyst.finance.trading import TradingEnvironment
|
||||||
from catalyst.pipeline.data import USEquityPricing, CryptoPricing
|
|
||||||
from catalyst.pipeline.loaders import (
|
|
||||||
USEquityPricingLoader,
|
|
||||||
CryptoPricingLoader,
|
|
||||||
)
|
|
||||||
from catalyst.utils.calendars import get_calendar
|
from catalyst.utils.calendars import get_calendar
|
||||||
from catalyst.utils.factory import create_simulation_parameters
|
from catalyst.utils.factory import create_simulation_parameters
|
||||||
|
from catalyst.data.loader import load_crypto_market_data
|
||||||
import catalyst.utils.paths as pth
|
import catalyst.utils.paths as pth
|
||||||
|
|
||||||
from catalyst.exchange.algorithm_exchange import ExchangeTradingAlgorithm
|
from catalyst.exchange.exchange_algorithm import ExchangeTradingAlgorithmLive, \
|
||||||
from catalyst.exchange.data_portal_exchange import DataPortalExchange
|
ExchangeTradingAlgorithmBacktest
|
||||||
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
|
from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \
|
||||||
|
DataPortalExchangeBacktest
|
||||||
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
|
from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
|
||||||
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
||||||
from catalyst.exchange.exchange_errors import (
|
from catalyst.exchange.exchange_errors import (
|
||||||
ExchangeRequestError,
|
ExchangeRequestError, ExchangeAuthEmpty,
|
||||||
ExchangeRequestErrorTooManyAttempts,
|
ExchangeRequestErrorTooManyAttempts,
|
||||||
BaseCurrencyNotFoundError)
|
BaseCurrencyNotFoundError, ExchangeNotFoundError)
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_auth, \
|
from catalyst.exchange.exchange_utils import get_exchange_auth, \
|
||||||
get_algo_object
|
get_algo_object, get_exchange_folder
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
log = Logger('run_algo')
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
log = Logger('run_algo', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
class _RunAlgoError(click.ClickException, ValueError):
|
class _RunAlgoError(click.ClickException, ValueError):
|
||||||
@@ -95,7 +93,8 @@ def _run(handle_data,
|
|||||||
live,
|
live,
|
||||||
exchange,
|
exchange,
|
||||||
algo_namespace,
|
algo_namespace,
|
||||||
base_currency):
|
base_currency,
|
||||||
|
live_graph):
|
||||||
"""Run a backtest for the given algorithm.
|
"""Run a backtest for the given algorithm.
|
||||||
|
|
||||||
This is shared between the cli and :func:`catalyst.run_algo`.
|
This is shared between the cli and :func:`catalyst.run_algo`.
|
||||||
@@ -147,72 +146,98 @@ def _run(handle_data,
|
|||||||
mode = 'live' if live else 'backtest'
|
mode = 'live' if live else 'backtest'
|
||||||
log.info('running algo in {mode} mode'.format(mode=mode))
|
log.info('running algo in {mode} mode'.format(mode=mode))
|
||||||
|
|
||||||
if live and exchange is not None:
|
|
||||||
exchange_name = exchange
|
exchange_name = exchange
|
||||||
start = pd.Timestamp.utcnow()
|
if exchange_name is None:
|
||||||
end = start + timedelta(minutes=1439)
|
raise ValueError('Please specify at least one exchange.')
|
||||||
|
|
||||||
|
exchange_list = [x.strip().lower() for x in exchange.split(',')]
|
||||||
|
|
||||||
|
exchanges = dict()
|
||||||
|
for exchange_name in exchange_list:
|
||||||
|
|
||||||
|
# Looking for the portfolio from the cache first
|
||||||
portfolio = get_algo_object(
|
portfolio = get_algo_object(
|
||||||
algo_name=algo_namespace,
|
algo_name=algo_namespace,
|
||||||
key='portfolio_{}'.format(exchange_name),
|
key='portfolio_{}'.format(exchange_name),
|
||||||
environ=environ
|
environ=environ
|
||||||
)
|
)
|
||||||
|
|
||||||
if portfolio is None:
|
if portfolio is None:
|
||||||
portfolio = ExchangePortfolio(
|
portfolio = ExchangePortfolio(
|
||||||
start_date=pd.Timestamp.utcnow()
|
start_date=pd.Timestamp.utcnow()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# This corresponds to the json file containing api token info
|
||||||
exchange_auth = get_exchange_auth(exchange_name)
|
exchange_auth = get_exchange_auth(exchange_name)
|
||||||
|
|
||||||
|
if live and (exchange_auth['key'] == '' \
|
||||||
|
or exchange_auth['secret'] == ''):
|
||||||
|
raise ExchangeAuthEmpty(
|
||||||
|
exchange=exchange_name.title(),
|
||||||
|
filename=os.path.join(
|
||||||
|
get_exchange_folder(exchange_name, environ), 'auth.json'))
|
||||||
|
|
||||||
if exchange_name == 'bitfinex':
|
if exchange_name == 'bitfinex':
|
||||||
exchange = Bitfinex(
|
exchanges[exchange_name] = Bitfinex(
|
||||||
key=exchange_auth['key'],
|
key=exchange_auth['key'],
|
||||||
secret=exchange_auth['secret'],
|
secret=exchange_auth['secret'],
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
portfolio=portfolio
|
portfolio=portfolio
|
||||||
)
|
)
|
||||||
elif exchange_name == 'bittrex':
|
elif exchange_name == 'bittrex':
|
||||||
exchange = Bittrex(
|
exchanges[exchange_name] = Bittrex(
|
||||||
|
key=exchange_auth['key'],
|
||||||
|
secret=exchange_auth['secret'],
|
||||||
|
base_currency=base_currency,
|
||||||
|
portfolio=portfolio
|
||||||
|
)
|
||||||
|
elif exchange_name == 'poloniex':
|
||||||
|
exchanges[exchange_name] = Poloniex(
|
||||||
key=exchange_auth['key'],
|
key=exchange_auth['key'],
|
||||||
secret=exchange_auth['secret'],
|
secret=exchange_auth['secret'],
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
portfolio=portfolio
|
portfolio=portfolio
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
||||||
'exchange not supported: %s' % exchange_name)
|
|
||||||
|
|
||||||
open_calendar = get_calendar('OPEN')
|
open_calendar = get_calendar('OPEN')
|
||||||
sim_params = create_simulation_parameters(
|
|
||||||
start=start,
|
|
||||||
end=end,
|
|
||||||
capital_base=capital_base,
|
|
||||||
data_frequency=data_frequency,
|
|
||||||
emission_rate=data_frequency,
|
|
||||||
)
|
|
||||||
|
|
||||||
if live and exchange is not None:
|
|
||||||
env = TradingEnvironment(
|
env = TradingEnvironment(
|
||||||
|
load=partial(
|
||||||
|
load_crypto_market_data,
|
||||||
|
environ=environ,
|
||||||
|
start_dt=start,
|
||||||
|
end_dt=end
|
||||||
|
),
|
||||||
environ=environ,
|
environ=environ,
|
||||||
exchange_tz='UTC',
|
exchange_tz='UTC',
|
||||||
asset_db_path=None
|
asset_db_path=None # We don't need an asset db, we have exchanges
|
||||||
)
|
)
|
||||||
env.asset_finder = AssetFinderExchange(exchange)
|
env.asset_finder = AssetFinderExchange()
|
||||||
|
choose_loader = None # TODO: use the DataPortal for in the algorithm class for this
|
||||||
|
|
||||||
data = DataPortalExchange(
|
if live:
|
||||||
exchange=exchange,
|
start = pd.Timestamp.utcnow()
|
||||||
|
|
||||||
|
# TODO: fix the end data.
|
||||||
|
end = start + timedelta(hours=8760)
|
||||||
|
|
||||||
|
data = DataPortalExchangeLive(
|
||||||
|
exchanges=exchanges,
|
||||||
asset_finder=env.asset_finder,
|
asset_finder=env.asset_finder,
|
||||||
trading_calendar=open_calendar,
|
trading_calendar=open_calendar,
|
||||||
first_trading_day=pd.to_datetime('today', utc=True)
|
first_trading_day=pd.to_datetime('today', utc=True)
|
||||||
)
|
)
|
||||||
choose_loader = None
|
|
||||||
|
|
||||||
def fetch_capital_base(attempt_index=0):
|
def fetch_capital_base(exchange, attempt_index=0):
|
||||||
"""
|
"""
|
||||||
Fetch the base currency amount required to bootstrap
|
Fetch the base currency amount required to bootstrap
|
||||||
the algorithm against the exchange.
|
the algorithm against the exchange.
|
||||||
|
|
||||||
The algorithm cannot continue without this value.
|
The algorithm cannot continue without this value.
|
||||||
|
|
||||||
|
:param exchange: the targeted exchange
|
||||||
:param attempt_index:
|
:param attempt_index:
|
||||||
:return capital_base: the amount of base currency available for
|
:return capital_base: the amount of base currency available for
|
||||||
trading
|
trading
|
||||||
@@ -223,8 +248,14 @@ def _run(handle_data,
|
|||||||
balances = exchange.get_balances()
|
balances = exchange.get_balances()
|
||||||
except ExchangeRequestError as e:
|
except ExchangeRequestError as e:
|
||||||
if attempt_index < 20:
|
if attempt_index < 20:
|
||||||
|
log.warn(
|
||||||
|
'could not retrieve balances on {}: {}'.format(
|
||||||
|
exchange.name, e
|
||||||
|
)
|
||||||
|
)
|
||||||
sleep(5)
|
sleep(5)
|
||||||
return fetch_capital_base(attempt_index + 1)
|
return fetch_capital_base(exchange, attempt_index + 1)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ExchangeRequestErrorTooManyAttempts(
|
raise ExchangeRequestErrorTooManyAttempts(
|
||||||
attempts=attempt_index,
|
attempts=attempt_index,
|
||||||
@@ -232,35 +263,86 @@ def _run(handle_data,
|
|||||||
)
|
)
|
||||||
|
|
||||||
if base_currency in balances:
|
if base_currency in balances:
|
||||||
return balances[base_currency]
|
base_currency_available = balances[base_currency]
|
||||||
|
log.info(
|
||||||
|
'base currency available in the account: {} {}'.format(
|
||||||
|
base_currency_available, base_currency
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if capital_base is not None \
|
||||||
|
and capital_base < base_currency_available:
|
||||||
|
log.info(
|
||||||
|
'using capital base limit: {} {}'.format(
|
||||||
|
capital_base, base_currency
|
||||||
|
)
|
||||||
|
)
|
||||||
|
amount = capital_base
|
||||||
|
else:
|
||||||
|
amount = base_currency_available
|
||||||
|
|
||||||
|
return amount
|
||||||
else:
|
else:
|
||||||
raise BaseCurrencyNotFoundError(
|
raise BaseCurrencyNotFoundError(
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
exchange=exchange_name
|
exchange=exchange_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
combined_capital_base = 0
|
||||||
|
for exchange_name in exchanges:
|
||||||
|
exchange = exchanges[exchange_name]
|
||||||
|
combined_capital_base += fetch_capital_base(exchange)
|
||||||
|
|
||||||
sim_params = create_simulation_parameters(
|
sim_params = create_simulation_parameters(
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
capital_base=fetch_capital_base(),
|
capital_base=capital_base,
|
||||||
emission_rate='minute',
|
emission_rate='minute',
|
||||||
data_frequency='minute'
|
data_frequency='minute'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: use the constructor instead
|
||||||
|
sim_params._arena = 'live'
|
||||||
|
|
||||||
|
algorithm_class = partial(
|
||||||
|
ExchangeTradingAlgorithmLive,
|
||||||
|
exchanges=exchanges,
|
||||||
|
algo_namespace=algo_namespace,
|
||||||
|
live_graph=live_graph
|
||||||
|
)
|
||||||
|
elif exchanges:
|
||||||
|
# Removed the existing Poloniex fork to keep things simple
|
||||||
|
# We can add back the complexity if required.
|
||||||
|
|
||||||
|
# I don't think that we should have arbitrary price data bundles
|
||||||
|
# Instead, we should center this data around exchanges.
|
||||||
|
# We still need to support bundles for other misc data, but we
|
||||||
|
# can handle this later.
|
||||||
|
|
||||||
|
data = DataPortalExchangeBacktest(
|
||||||
|
exchange_names=[exchange_name for exchange_name in exchanges],
|
||||||
|
asset_finder=None,
|
||||||
|
trading_calendar=open_calendar,
|
||||||
|
first_trading_day=start,
|
||||||
|
last_available_session=end
|
||||||
|
)
|
||||||
|
|
||||||
|
sim_params = create_simulation_parameters(
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
capital_base=capital_base,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
emission_rate=data_frequency,
|
||||||
|
)
|
||||||
|
|
||||||
|
algorithm_class = partial(
|
||||||
|
ExchangeTradingAlgorithmBacktest,
|
||||||
|
exchanges=exchanges
|
||||||
|
)
|
||||||
|
|
||||||
elif bundle is not None:
|
elif bundle is not None:
|
||||||
bundles = bundle.split(',')
|
|
||||||
|
|
||||||
def get_trading_env_and_data(bundles):
|
|
||||||
env = data = None
|
|
||||||
|
|
||||||
b = 'poloniex'
|
|
||||||
if len(bundles) == 0:
|
|
||||||
return env, data
|
|
||||||
elif len(bundles) == 1:
|
|
||||||
b = bundles[0]
|
|
||||||
|
|
||||||
bundle_data = load(
|
bundle_data = load(
|
||||||
b,
|
bundle,
|
||||||
environ,
|
environ,
|
||||||
bundle_timestamp,
|
bundle_timestamp,
|
||||||
)
|
)
|
||||||
@@ -276,72 +358,19 @@ def _run(handle_data,
|
|||||||
str(bundle_data.asset_finder.engine.url),
|
str(bundle_data.asset_finder.engine.url),
|
||||||
)
|
)
|
||||||
|
|
||||||
env = TradingEnvironment(
|
env = TradingEnvironment(asset_db_path=connstr, environ=environ)
|
||||||
load=partial(load_crypto_market_data, bundle=b, bundle_data=bundle_data, environ=environ),
|
first_trading_day = \
|
||||||
bm_symbol='USDT_BTC',
|
bundle_data.equity_minute_bar_reader.first_trading_day
|
||||||
trading_calendar=open_calendar,
|
|
||||||
asset_db_path=connstr,
|
|
||||||
environ=environ,
|
|
||||||
)
|
|
||||||
|
|
||||||
first_trading_day = bundle_data.minute_bar_reader.first_trading_day
|
|
||||||
|
|
||||||
data = DataPortal(
|
data = DataPortal(
|
||||||
env.asset_finder,
|
env.asset_finder, open_calendar,
|
||||||
open_calendar,
|
|
||||||
first_trading_day=first_trading_day,
|
first_trading_day=first_trading_day,
|
||||||
minute_reader=bundle_data.minute_bar_reader,
|
equity_minute_reader=bundle_data.equity_minute_bar_reader,
|
||||||
five_minute_reader=bundle_data.five_minute_bar_reader,
|
equity_daily_reader=bundle_data.equity_daily_bar_reader,
|
||||||
daily_reader=bundle_data.daily_bar_reader,
|
|
||||||
adjustment_reader=bundle_data.adjustment_reader,
|
adjustment_reader=bundle_data.adjustment_reader,
|
||||||
)
|
)
|
||||||
|
|
||||||
return env, data
|
perf = algorithm_class(
|
||||||
|
|
||||||
def get_loader_for_bundle(b):
|
|
||||||
bundle_data = load(
|
|
||||||
b,
|
|
||||||
environ,
|
|
||||||
bundle_timestamp,
|
|
||||||
)
|
|
||||||
|
|
||||||
if b == 'poloniex':
|
|
||||||
return CryptoPricingLoader(
|
|
||||||
bundle_data,
|
|
||||||
data_frequency,
|
|
||||||
CryptoPricing,
|
|
||||||
)
|
|
||||||
elif b == 'quandl':
|
|
||||||
return USEquityPricingLoader(
|
|
||||||
bundle_data,
|
|
||||||
data_frequency,
|
|
||||||
USEquityPricing,
|
|
||||||
)
|
|
||||||
raise ValueError(
|
|
||||||
"No PipelineLoader registered for bundle %s." % b
|
|
||||||
)
|
|
||||||
|
|
||||||
loaders = [get_loader_for_bundle(b) for b in bundles]
|
|
||||||
env, data = get_trading_env_and_data(bundles)
|
|
||||||
|
|
||||||
def choose_loader(column):
|
|
||||||
for loader in loaders:
|
|
||||||
if column in loader.columns:
|
|
||||||
return loader
|
|
||||||
raise ValueError(
|
|
||||||
"No PipelineLoader registered for column %s." % column
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
env = TradingEnvironment(environ=environ)
|
|
||||||
choose_loader = None
|
|
||||||
|
|
||||||
TradingAlgorithmClass = (
|
|
||||||
partial(ExchangeTradingAlgorithm, exchange=exchange,
|
|
||||||
algo_namespace=algo_namespace)
|
|
||||||
if live and exchange else TradingAlgorithm)
|
|
||||||
|
|
||||||
perf = TradingAlgorithmClass(
|
|
||||||
namespace=namespace,
|
namespace=namespace,
|
||||||
env=env,
|
env=env,
|
||||||
get_pipeline_loader=choose_loader,
|
get_pipeline_loader=choose_loader,
|
||||||
@@ -439,7 +468,9 @@ def run_algorithm(initialize,
|
|||||||
live=False,
|
live=False,
|
||||||
exchange_name=None,
|
exchange_name=None,
|
||||||
base_currency=None,
|
base_currency=None,
|
||||||
algo_namespace=None):
|
algo_namespace=None,
|
||||||
|
live_graph=False,
|
||||||
|
output=os.devnull):
|
||||||
"""Run a trading algorithm.
|
"""Run a trading algorithm.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -509,8 +540,15 @@ def run_algorithm(initialize,
|
|||||||
--------
|
--------
|
||||||
catalyst.data.bundles.bundles : The available data bundles.
|
catalyst.data.bundles.bundles : The available data bundles.
|
||||||
"""
|
"""
|
||||||
load_extensions(default_extension, extensions, strict_extensions, environ)
|
load_extensions(
|
||||||
|
default_extension, extensions, strict_extensions, environ
|
||||||
|
)
|
||||||
|
|
||||||
|
# I'm not sure that we need this since the modified DataPortal
|
||||||
|
# does not require extensions to be explicitly loaded.
|
||||||
|
|
||||||
|
# This will be useful for arbitrary non-pricing bundles but we may
|
||||||
|
# need to modify the logic.
|
||||||
if not live:
|
if not live:
|
||||||
non_none_data = valfilter(bool, {
|
non_none_data = valfilter(bool, {
|
||||||
'data': data is not None,
|
'data': data is not None,
|
||||||
@@ -545,12 +583,13 @@ def run_algorithm(initialize,
|
|||||||
bundle_timestamp=bundle_timestamp,
|
bundle_timestamp=bundle_timestamp,
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
output=os.devnull,
|
output=output,
|
||||||
print_algo=False,
|
print_algo=False,
|
||||||
local_namespace=False,
|
local_namespace=False,
|
||||||
environ=environ,
|
environ=environ,
|
||||||
live=live,
|
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=live_graph
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
www.zipline.io
|
enigma-catalyst.readthedocs.io
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<h1>Live Trading</h1>
|
|
||||||
This document explains how to get started with live trading.
|
|
||||||
|
|
||||||
<h2>Supported Exchanges</h2>
|
|
||||||
Catalyst can trade against these exchanges:
|
|
||||||
|
|
||||||
* Bitfinex, id=`bitfinex`
|
|
||||||
* Bittrex, id=`bittrex`
|
|
||||||
|
|
||||||
<h3>Authentication</h3>
|
|
||||||
Most exchanges require key/token combination for authentication. By
|
|
||||||
convention, Catalyst uses an "auth.json" file to hold this data.
|
|
||||||
|
|
||||||
This example illustrates the convention using the Bitfinex exchange.
|
|
||||||
Here is how to generate key and secret values for bitfinex:
|
|
||||||
https://docs.bitfinex.com/v1/docs/api-access. Most exchanges follow
|
|
||||||
a similar process.
|
|
||||||
|
|
||||||
The auth.json file:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "bitfinex",
|
|
||||||
"key": "my-key",
|
|
||||||
"secret": "my-secret"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The file goes here:
|
|
||||||
```
|
|
||||||
~/.catalyst/data/exchanges/bitfinex/auth.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that the 'bitfinex' directory corresponds to the id of the Bitfinex
|
|
||||||
exchange as defined in the "Supported Exchanges" section above.
|
|
||||||
Attempting to run an algorithm where the targeted exchange is missing
|
|
||||||
its "auth.json" file will create the directory structure but result
|
|
||||||
in an error.
|
|
||||||
|
|
||||||
<h3>Currency Symbols</h3>
|
|
||||||
Catalyst introduces a universal convention to reference
|
|
||||||
trading pairs and individual currencies. This
|
|
||||||
is required to ensure that the `symbol()` api predictably
|
|
||||||
returns the correct asset regardless of the targeted exchange.
|
|
||||||
|
|
||||||
Exchanges tend to use their own convention to represent currencies
|
|
||||||
(e.g. XBT and BTC both represent Bitcoin on different exchanges).
|
|
||||||
Trading pairs are also inconsistent. 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]* all lowercase.
|
|
||||||
|
|
||||||
Currency symbols (e.g. btc, eth, ltc) follow the Bittrex convention.
|
|
||||||
|
|
||||||
Here are some examples:
|
|
||||||
```python
|
|
||||||
# With Bitfinex
|
|
||||||
bitcoin_usd_asset = symbol('btc_usd')
|
|
||||||
ethereum_bitcoin_asset = symbol('eth_btc')
|
|
||||||
|
|
||||||
# With Bittrex
|
|
||||||
ethereum_bitcoin_asset = symbol('eth_btc')
|
|
||||||
neo_ethereum_asset = symbol('neo_eth)
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that the trading pairs are always referenced in the same manner.
|
|
||||||
However, not all trading pairs are available on all exchanges. An
|
|
||||||
error will occur if the specified trading pair is not trading
|
|
||||||
on the exchange.
|
|
||||||
|
|
||||||
<h2>Trading an Algorithm</h2>
|
|
||||||
There is no special convention to follow when writing an
|
|
||||||
algorithm for live trading. The same algorithm should work in
|
|
||||||
backtest and live execution mode without modification.
|
|
||||||
|
|
||||||
What differs are the arguments provided to the catalyst client or
|
|
||||||
`run_algorithm()` interface. Here is example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
run_algorithm(
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='bitfinex',
|
|
||||||
live=True,
|
|
||||||
algo_namespace='my_algo_trading_xrp',
|
|
||||||
base_currency='btc'
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Here is the breakdown of the new arguments:
|
|
||||||
* live: Boolean flag which enables live trading.
|
|
||||||
* exchange_name: The name of the targeted exchange
|
|
||||||
(supported values: *bitfinex*, *bittrex*).
|
|
||||||
* algo_namespace: A arbitrary label assigned to your algorithm for
|
|
||||||
data storage purposes.
|
|
||||||
* base_currency: The base currency used to calculate the
|
|
||||||
statistics of your algorithm. Currently, the base currency of all
|
|
||||||
trading pairs of your algorithm must match this value.
|
|
||||||
|
|
||||||
Here is a complete algorithm for reference:
|
|
||||||
[Buy Low and Sell High](../catalyst/examples/buy_low_sell_high_live.py)
|
|
||||||
+555
-469
File diff suppressed because it is too large
Load Diff
+11
-10
@@ -1,7 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from zipline import __version__ as version
|
#from catalyst import __version__ as version
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
@@ -21,14 +21,14 @@ extensions = [
|
|||||||
|
|
||||||
|
|
||||||
extlinks = {
|
extlinks = {
|
||||||
'issue': ('https://github.com/quantopian/zipline/issues/%s', '#'),
|
'issue': ('https://github.com/enigmampc/catalyst/issues/%s', '#'),
|
||||||
'commit': ('https://github.com/quantopian/zipline/commit/%s', ''),
|
'commit': ('https://github.com/enigmampc/catalyst/commit/%s', ''),
|
||||||
}
|
}
|
||||||
|
|
||||||
# -- Docstrings ---------------------------------------------------------------
|
# -- Docstrings ---------------------------------------------------------------
|
||||||
|
|
||||||
extensions += ['numpydoc']
|
#extensions += ['numpydoc']
|
||||||
numpydoc_show_class_members = False
|
#numpydoc_show_class_members = False
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['.templates']
|
templates_path = ['.templates']
|
||||||
@@ -40,11 +40,12 @@ source_suffix = '.rst'
|
|||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'Zipline'
|
project = u'Catalyst'
|
||||||
copyright = u'2016, Quantopian Inc.'
|
copyright = u'2017, Enigma MPC, Inc.'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags, but excluding the commit hash
|
# The full version, including alpha/beta/rc tags, but excluding the commit hash
|
||||||
release = version.split('+', 1)[0]
|
#release = version.split('+', 1)[0]
|
||||||
|
release = '0.3'
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
@@ -84,7 +85,7 @@ html_show_sphinx = True
|
|||||||
html_show_copyright = True
|
html_show_copyright = True
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = 'ziplinedoc'
|
htmlhelp_basename = 'catalystdoc'
|
||||||
|
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'http://docs.python.org/dev': None,
|
'http://docs.python.org/dev': None,
|
||||||
@@ -93,6 +94,6 @@ intersphinx_mapping = {
|
|||||||
'pandas': ('http://pandas.pydata.org/pandas-docs/stable/', None),
|
'pandas': ('http://pandas.pydata.org/pandas-docs/stable/', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
doctest_global_setup = "import zipline"
|
doctest_global_setup = "import catalyst"
|
||||||
|
|
||||||
todo_include_todos = True
|
todo_include_todos = True
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
Development Guidelines
|
Development Guidelines
|
||||||
======================
|
======================
|
||||||
This page is intended for developers of Zipline, people who want to contribute to the Zipline codebase or documentation, or people who want to install from source and make local changes to their copy of Zipline.
|
This page is intended for developers of Catalyst, people who want to contribute to the Catalyst codebase or documentation, or people who want to install from source and make local changes to their copy of Catalyst.
|
||||||
|
|
||||||
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We `track issues`__ on `GitHub`__ and also have a `mailing list`__ where you can ask questions.
|
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We `track issues <https://github.com/enigmampc/catalyst/issues>`_ on `GitHub <https://github.com/enigmampc/catalyst>`_ and also have a `discord group <https://discord.gg/SJK32GY>`_ where you can ask questions.
|
||||||
|
|
||||||
__ https://github.com/quantopian/zipline/issues
|
|
||||||
__ https://github.com/
|
|
||||||
__ https://groups.google.com/forum/#!forum/zipline
|
|
||||||
|
|
||||||
Creating a Development Environment
|
Creating a Development Environment
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
First, you'll need to clone Zipline by running:
|
First, you'll need to clone Catalyst by running:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ git clone git@github.com:your-github-username/zipline.git
|
$ git clone git@github.com:enigmampc/catalyst.git
|
||||||
|
|
||||||
Then check out to a new branch where you can make your changes:
|
Then check out to a new branch where you can make your changes:
|
||||||
|
|
||||||
@@ -23,15 +19,13 @@ Then check out to a new branch where you can make your changes:
|
|||||||
|
|
||||||
$ git checkout -b some-short-descriptive-name
|
$ git checkout -b some-short-descriptive-name
|
||||||
|
|
||||||
If you don't already have them, you'll need some C library dependencies. You can follow the `install guide`__ to get the appropriate dependencies.
|
If you don't already have them, you'll need some C library dependencies. You can follow the `install guide <install.html>`_ to get the appropriate dependencies.
|
||||||
|
|
||||||
__ install.html
|
|
||||||
|
|
||||||
The following section assumes you already have virtualenvwrapper and pip installed on your system. Suggested installation of Python library dependencies used for development:
|
The following section assumes you already have virtualenvwrapper and pip installed on your system. Suggested installation of Python library dependencies used for development:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ mkvirtualenv zipline
|
$ mkvirtualenv catalyst
|
||||||
$ ./etc/ordered_pip.sh ./etc/requirements.txt
|
$ ./etc/ordered_pip.sh ./etc/requirements.txt
|
||||||
$ pip install -r ./etc/requirements_dev.txt
|
$ pip install -r ./etc/requirements_dev.txt
|
||||||
$ pip install -r ./etc/requirements_blaze.txt
|
$ pip install -r ./etc/requirements_blaze.txt
|
||||||
@@ -42,104 +36,39 @@ Finally, you can build the C extensions by running:
|
|||||||
|
|
||||||
$ python setup.py build_ext --inplace
|
$ python setup.py build_ext --inplace
|
||||||
|
|
||||||
To finish, make sure `tests`__ pass.
|
.. To finish, make sure `tests`__ pass.
|
||||||
|
|
||||||
__ #style-guide-running-tests
|
.. __ #style-guide-running-tests
|
||||||
|
|
||||||
If you get an error running nosetests after setting up a fresh virtualenv, please try running
|
.. If you get an error running nosetests after setting up a fresh virtualenv, please try running
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block
|
||||||
|
|
||||||
# where zipline is the name of your virtualenv
|
.. # where zipline is the name of your virtualenv
|
||||||
$ deactivate zipline
|
.. $ deactivate zipline
|
||||||
$ workon zipline
|
.. $ workon zipline
|
||||||
|
|
||||||
|
|
||||||
Development with Docker
|
.. Development with Docker
|
||||||
|
.. -----------------------
|
||||||
|
|
||||||
|
..If you want to work with zipline using a `Docker`__ container, you'll need to build the ``Dockerfile`` in the Zipline root directory, and then build ``Dockerfile-dev``. Instructions for building both containers can be found in ``Dockerfile`` and ``Dockerfile-dev``, respectively.
|
||||||
|
|
||||||
|
.. __ https://docs.docker.com/get-started/
|
||||||
|
|
||||||
|
Git Branching Structure
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
If you want to work with zipline using a `Docker`__ container, you'll need to build the ``Dockerfile`` in the Zipline root directory, and then build ``Dockerfile-dev``. Instructions for building both containers can be found in ``Dockerfile`` and ``Dockerfile-dev``, respectively.
|
If you want to contribute to the codebase of Catalyst, familiarize yourself with our branching structure, a fairly standardized one for that matter, that follows what is documented in the following article: `A successful Git branching model <http://nvie.com/posts/a-successful-git-branching-model/>`_. To contribute, create your local branch and submit a Pull Request (PR) to the **develop** branch.
|
||||||
|
|
||||||
__ https://docs.docker.com/get-started/
|
.. image:: https://camo.githubusercontent.com/9bde6fb64a9542a572e0e2017cbb58d9d2c440ac/687474703a2f2f6e7669652e636f6d2f696d672f6769742d6d6f64656c4032782e706e67
|
||||||
|
|
||||||
|
|
||||||
Style Guide & Running Tests
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
We use `flake8`__ for checking style requirements and `nosetests`__ to run Zipline tests. Our `continuous integration`__ tools will run these commands.
|
|
||||||
|
|
||||||
__ http://flake8.pycqa.org/en/latest/
|
|
||||||
__ http://nose.readthedocs.io/en/latest/
|
|
||||||
__ https://en.wikipedia.org/wiki/Continuous_integration
|
|
||||||
|
|
||||||
Before submitting patches or pull requests, please ensure that your changes pass when running:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ flake8 zipline tests
|
|
||||||
|
|
||||||
In order to run tests locally, you'll need `TA-lib`__, which you can install on Linux by running:
|
|
||||||
|
|
||||||
__ https://mrjbq7.github.io/ta-lib/install.html
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
|
||||||
$ tar -xvzf ta-lib-0.4.0-src.tar.gz
|
|
||||||
$ cd ta-lib/
|
|
||||||
$ ./configure --prefix=/usr
|
|
||||||
$ make
|
|
||||||
$ sudo make install
|
|
||||||
|
|
||||||
And for ``TA-lib`` on OS X you can just run:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ brew install ta-lib
|
|
||||||
|
|
||||||
Then run ``pip install`` TA-lib:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ pip install -r ./etc/requirements_talib.txt
|
|
||||||
|
|
||||||
You should now be free to run tests:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ nosetests
|
|
||||||
|
|
||||||
|
|
||||||
Continuous Integration
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
We use `Travis CI`__ for Linux-64 bit builds and `AppVeyor`__ for Windows-64 bit builds.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
We do not currently have CI for OSX-64 bit builds. 32-bit builds may work but are not included in our integration tests.
|
|
||||||
|
|
||||||
__ https://travis-ci.org/quantopian/zipline
|
|
||||||
__ https://ci.appveyor.com/project/quantopian/zipline
|
|
||||||
|
|
||||||
|
|
||||||
Packaging
|
|
||||||
---------
|
|
||||||
To learn about how we build Zipline conda packages, you can read `this`__ section in our release process notes.
|
|
||||||
|
|
||||||
__ release-process.html#uploading-conda-packages
|
|
||||||
|
|
||||||
Contributing to the Docs
|
Contributing to the Docs
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
If you'd like to contribute to the documentation on zipline.io, you can navigate to ``docs/source/`` where each `reStructuredText`__ (``.rst``) file is a separate section there. To add a section, create a new file called ``some-descriptive-name.rst`` and add ``some-descriptive-name`` to ``appendix.rst``. To edit a section, simply open up one of the existing files, make your changes, and save them.
|
If you'd like to contribute to the documentation on enigmampc.github.io, you can navigate to ``docs/source/`` where each `reStructuredText <https://en.wikipedia.org/wiki/ReStructuredText>`_ file is a separate section there. To add a section, create a new file called ``some-descriptive-name.rst`` and add ``some-descriptive-name`` to ``index.rst``. To edit a section, simply open up one of the existing files, make your changes, and save them.
|
||||||
|
|
||||||
__ https://en.wikipedia.org/wiki/ReStructuredText
|
|
||||||
|
|
||||||
We use `Sphinx`__ to generate documentation for Zipline, which you will need to install by running:
|
|
||||||
|
|
||||||
__ http://www.sphinx-doc.org/en/stable/
|
|
||||||
|
|
||||||
|
We use `Sphinx <http://www.sphinx-doc.org/en/stable/>`_ to generate documentation for Catalyst, which you will need to install by running:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
@@ -149,7 +78,7 @@ To build and view the docs locally, run:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
# assuming you're in the Zipline root directory
|
# assuming you're in the Catalyst root directory
|
||||||
$ cd docs
|
$ cd docs
|
||||||
$ make html
|
$ make html
|
||||||
$ {BROWSER} build/html/index.html
|
$ {BROWSER} build/html/index.html
|
||||||
@@ -162,7 +91,7 @@ Standard prefixes to start a commit message:
|
|||||||
|
|
||||||
.. code-block:: text
|
.. code-block:: text
|
||||||
|
|
||||||
BLD: change related to building Zipline
|
BLD: change related to building Catalyst
|
||||||
BUG: bug fix
|
BUG: bug fix
|
||||||
DEP: deprecate something, or remove a deprecated object
|
DEP: deprecate something, or remove a deprecated object
|
||||||
DEV: development tool or utility
|
DEV: development tool or utility
|
||||||
@@ -172,15 +101,13 @@ Standard prefixes to start a commit message:
|
|||||||
REV: revert an earlier commit
|
REV: revert an earlier commit
|
||||||
STY: style fix (whitespace, PEP8, flake8, etc)
|
STY: style fix (whitespace, PEP8, flake8, etc)
|
||||||
TST: addition or modification of tests
|
TST: addition or modification of tests
|
||||||
REL: related to releasing Zipline
|
REL: related to releasing Catalyst
|
||||||
PERF: performance enhancements
|
PERF: performance enhancements
|
||||||
|
|
||||||
|
|
||||||
Some commit style guidelines:
|
Some commit style guidelines:
|
||||||
|
|
||||||
Commit lines should be no longer than `72 characters`__. The first line of the commit should include one of the above prefixes. There should be an empty line between the commit subject and the body of the commit. In general, the message should be in the imperative tense. Best practice is to include not only what the change is, but why the change was made.
|
Commit lines should be no longer than `72 characters <https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project>`_. The first line of the commit should include one of the above prefixes. There should be an empty line between the commit subject and the body of the commit. In general, the message should be in the imperative tense. Best practice is to include not only what the change is, but why the change was made.
|
||||||
|
|
||||||
__ https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project
|
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
@@ -203,8 +130,6 @@ __ https://git-scm.com/book/en/v2/Distributed-Git-Contributing-to-a-Project
|
|||||||
Formatting Docstrings
|
Formatting Docstrings
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
When adding or editing docstrings for classes, functions, etc, we use `numpy`__ as the canonical reference.
|
When adding or editing docstrings for classes, functions, etc, we use `numpy <https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt>`_ as the canonical reference.
|
||||||
|
|
||||||
__ https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user